@sugar-crash-studios/vibe-forge 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/clear-attention.md +63 -0
- package/.claude/commands/compact-context.md +52 -0
- package/.claude/commands/configure-vcs.md +102 -0
- package/.claude/commands/forge.md +171 -0
- package/.claude/commands/need-help.md +77 -0
- package/.claude/commands/update-status.md +64 -0
- package/.claude/commands/worker-loop.md +106 -0
- package/.claude/hooks/worker-loop.js +198 -0
- package/.claude/scripts/setup-worker-loop.sh +45 -0
- package/.claude/settings.local.json +46 -0
- package/LICENSE +21 -0
- package/README.md +238 -0
- package/agents/aegis/personality.md +294 -0
- package/agents/anvil/personality.md +276 -0
- package/agents/architect/personality.md +258 -0
- package/agents/crucible/personality.md +360 -0
- package/agents/ember/personality.md +291 -0
- package/agents/forge-master/capabilities.md +144 -0
- package/agents/forge-master/context-template.md +128 -0
- package/agents/forge-master/personality.md +138 -0
- package/agents/furnace/personality.md +340 -0
- package/agents/herald/personality.md +247 -0
- package/agents/loki/personality.md +108 -0
- package/agents/oracle/personality.md +283 -0
- package/agents/pixel/personality.md +113 -0
- package/agents/planning-hub/personality.md +320 -0
- package/agents/scribe/personality.md +251 -0
- package/agents/temper/personality.md +218 -0
- package/bin/cli.js +375 -0
- package/bin/dashboard/api/agents.js +333 -0
- package/bin/dashboard/api/dispatch.js +483 -0
- package/bin/dashboard/api/tasks.js +416 -0
- package/bin/dashboard/frontend/index.html +13 -0
- package/bin/dashboard/frontend/package.json +16 -0
- package/bin/dashboard/frontend/src/App.svelte +222 -0
- package/bin/dashboard/frontend/src/app.css +1777 -0
- package/bin/dashboard/frontend/src/lib/components/AgentCard.svelte +60 -0
- package/bin/dashboard/frontend/src/lib/components/AgentsPanel.svelte +57 -0
- package/bin/dashboard/frontend/src/lib/components/DispatchModal.svelte +180 -0
- package/bin/dashboard/frontend/src/lib/components/Footer.svelte +33 -0
- package/bin/dashboard/frontend/src/lib/components/Header.svelte +84 -0
- package/bin/dashboard/frontend/src/lib/components/IssueCard.svelte +33 -0
- package/bin/dashboard/frontend/src/lib/components/IssuesPanel.svelte +73 -0
- package/bin/dashboard/frontend/src/lib/components/KeyboardShortcutsModal.svelte +108 -0
- package/bin/dashboard/frontend/src/lib/components/MobileTabs.svelte +52 -0
- package/bin/dashboard/frontend/src/lib/components/NotificationCard.svelte +60 -0
- package/bin/dashboard/frontend/src/lib/components/NotificationsPanel.svelte +44 -0
- package/bin/dashboard/frontend/src/lib/components/TaskCard.svelte +63 -0
- package/bin/dashboard/frontend/src/lib/components/TasksPanel.svelte +82 -0
- package/bin/dashboard/frontend/src/lib/components/Toast.svelte +45 -0
- package/bin/dashboard/frontend/src/lib/stores/agents.js +34 -0
- package/bin/dashboard/frontend/src/lib/stores/issues.js +54 -0
- package/bin/dashboard/frontend/src/lib/stores/notifications.js +48 -0
- package/bin/dashboard/frontend/src/lib/stores/tasks.js +63 -0
- package/bin/dashboard/frontend/src/lib/stores/theme.js +33 -0
- package/bin/dashboard/frontend/src/lib/stores/toast.js +35 -0
- package/bin/dashboard/frontend/src/lib/stores/ui.js +25 -0
- package/bin/dashboard/frontend/src/lib/stores/voice.js +275 -0
- package/bin/dashboard/frontend/src/lib/stores/websocket.js +295 -0
- package/bin/dashboard/frontend/src/lib/utils/api.js +101 -0
- package/bin/dashboard/frontend/src/lib/utils/formatters.js +54 -0
- package/bin/dashboard/frontend/src/main.js +9 -0
- package/bin/dashboard/frontend/svelte.config.js +5 -0
- package/bin/dashboard/frontend/vite.config.js +20 -0
- package/bin/dashboard/public/assets/index-DnfVj9Ce.css +1 -0
- package/bin/dashboard/public/assets/index-Ze5h0kXQ.js +2 -0
- package/bin/dashboard/public/index.html +14 -0
- package/bin/dashboard/server.js +566 -0
- package/bin/forge-daemon.sh +463 -0
- package/bin/forge-setup.sh +645 -0
- package/bin/forge-spawn.sh +164 -0
- package/bin/forge.cmd +83 -0
- package/bin/forge.sh +533 -0
- package/bin/lib/agents.sh +177 -0
- package/bin/lib/colors.sh +44 -0
- package/bin/lib/config.sh +347 -0
- package/bin/lib/constants.sh +241 -0
- package/bin/lib/daemon/display.sh +128 -0
- package/bin/lib/daemon/notifications.sh +263 -0
- package/bin/lib/daemon/routing.sh +77 -0
- package/bin/lib/daemon/state.sh +115 -0
- package/bin/lib/daemon/sync.sh +95 -0
- package/bin/lib/database.sh +310 -0
- package/bin/lib/heimdall-setup.js +113 -0
- package/bin/lib/heimdall.js +265 -0
- package/bin/lib/json.sh +264 -0
- package/bin/lib/terminal.js +451 -0
- package/bin/lib/util.sh +126 -0
- package/bin/lib/vcs.js +349 -0
- package/config/agent-manifest.yaml +203 -0
- package/config/agents.json +168 -0
- package/config/task-template.md +159 -0
- package/config/task-types.yaml +106 -0
- package/context/agent-status/aegis.json +7 -0
- package/context/agent-status/anvil.json +7 -0
- package/context/agent-status/architect.json +7 -0
- package/context/agent-status/crucible.json +7 -0
- package/context/agent-status/ember.json +7 -0
- package/context/agent-status/furnace.json +7 -0
- package/context/agent-status/loki.json +7 -0
- package/context/agent-status/oracle.json +7 -0
- package/context/agent-status/pixel.json +7 -0
- package/context/agent-status/planning-hub.json +7 -0
- package/context/agent-status/scribe.json +7 -0
- package/context/agent-status/temper.json +7 -0
- package/context/feature-brainstorm.md +426 -0
- package/context/forge-state.yaml +19 -0
- package/context/modern-conventions.md +129 -0
- package/context/project-context-template.md +122 -0
- package/context/project-context.md +122 -0
- package/docs/TODO.md +150 -0
- package/docs/agents.md +409 -0
- package/docs/architecture/decisions/ADR-001-daemon-modularization.md +122 -0
- package/docs/architecture/vibe-lab-integration.md +684 -0
- package/docs/architecture.md +194 -0
- package/docs/bmad-gap-analysis-2026-03-31.md +444 -0
- package/docs/cleanup-workflow.md +329 -0
- package/docs/commands.md +451 -0
- package/docs/dashboard-mockup.html +989 -0
- package/docs/getting-started.md +261 -0
- package/docs/integration/forge-ownership-policy.md +112 -0
- package/docs/npm-publishing.md +132 -0
- package/docs/roadmap-2026.md +519 -0
- package/docs/security.md +144 -0
- package/docs/wireframes/dashboard-mvp.md +1164 -0
- package/docs/workflows/README.md +32 -0
- package/docs/workflows/azure-devops.md +108 -0
- package/docs/workflows/bitbucket.md +104 -0
- package/docs/workflows/git-only.md +130 -0
- package/docs/workflows/gitea.md +168 -0
- package/docs/workflows/github.md +103 -0
- package/docs/workflows/gitlab.md +105 -0
- package/docs/workflows.md +454 -0
- package/package.json +73 -0
- package/tasks/completed/ARCH-001-duplicate-agent-config.md +121 -0
- package/tasks/completed/ARCH-002-mixed-bash-node-implementation.md +88 -0
- package/tasks/completed/ARCH-003-worker-loop-hook-duplication.md +77 -0
- package/tasks/completed/ARCH-009-test-organization.md +78 -0
- package/tasks/completed/ARCH-011-jq-vs-nodejs-json.md +94 -0
- package/tasks/completed/ARCH-012-tmp-files-in-root.md +71 -0
- package/tasks/completed/ARCH-013-exit-code-constants.md +65 -0
- package/tasks/completed/ARCH-014-sed-incompatibility.md +96 -0
- package/tasks/completed/ARCH-015-docs-todo-tracking.md +83 -0
- package/tasks/completed/BUG-dash-001-tasks-filter-error.md +31 -0
- package/tasks/completed/BUG-dash-002-agents-unknown.md +41 -0
- package/tasks/completed/CLEAN-001.md +38 -0
- package/tasks/completed/CLEAN-002.md +43 -0
- package/tasks/completed/CLEAN-003.md +47 -0
- package/tasks/completed/CLEAN-004.md +56 -0
- package/tasks/completed/CLEAN-005.md +75 -0
- package/tasks/completed/CLEAN-006.md +47 -0
- package/tasks/completed/CLEAN-007.md +34 -0
- package/tasks/completed/CLEAN-008.md +49 -0
- package/tasks/completed/CLEAN-012.md +58 -0
- package/tasks/completed/CLEAN-013.md +45 -0
- package/tasks/completed/FEATURE-001a-dashboard-wireframes.md +162 -0
- package/tasks/completed/IMPL-007a-daemon-notifications-module.md +82 -0
- package/tasks/completed/IMPL-007b-daemon-sync-module.md +71 -0
- package/tasks/completed/IMPL-007c-daemon-state-module.md +80 -0
- package/tasks/completed/IMPL-007d-daemon-routing-module.md +77 -0
- package/tasks/completed/IMPL-007e-daemon-display-module.md +77 -0
- package/tasks/completed/IMPL-007f-daemon-integration.md +124 -0
- package/tasks/completed/PLAT-1-heimdall.md +420 -0
- package/tasks/completed/SEC-001-sql-injection-fix.md +58 -0
- package/tasks/completed/SEC-002-notification-injection-fix.md +45 -0
- package/tasks/completed/SEC-003-eval-injection-fix.md +54 -0
- package/tasks/completed/SEC-004-pid-race-condition-fix.md +49 -0
- package/tasks/completed/SEC-005-worker-loop-path-fix.md +51 -0
- package/tasks/completed/SEC-006-eval-agent-names.md +55 -0
- package/tasks/completed/SEC-007-spawn-escaping.md +67 -0
- package/tasks/completed/TASK-DASH-001-server-infrastructure.md +185 -0
- package/tasks/completed/TASK-anvil-001-dashboard-frontend.md +133 -0
- package/tasks/completed/review-bmad-aegis.md +89 -0
- package/tasks/completed/review-bmad-anvil.md +80 -0
- package/tasks/completed/review-bmad-crucible.md +81 -0
- package/tasks/completed/review-bmad-ember.md +90 -0
- package/tasks/completed/review-bmad-furnace.md +79 -0
- package/tasks/completed/review-bmad-pixel.md +82 -0
- package/tasks/completed/review-bmad-scribe.md +92 -0
- package/tasks/completed/review-bmad-sentinel.md +83 -0
- package/tasks/pending/ARCH-004-git-bash-detection-duplication.md +72 -0
- package/tasks/pending/ARCH-005-missing-src-directory.md +95 -0
- package/tasks/pending/ARCH-006-task-template-location.md +64 -0
- package/tasks/pending/ARCH-008-forge-master-vs-hub.md +81 -0
- package/tasks/pending/ARCH-010-missing-index-files.md +84 -0
- package/tasks/pending/CLEAN-009.md +31 -0
- package/tasks/pending/CLEAN-010.md +30 -0
- package/tasks/pending/CLEAN-011.md +30 -0
- package/tasks/pending/CLEAN-014.md +32 -0
- package/tasks/pending/DESIGN-dash-001-layout-review.md +45 -0
- package/tasks/pending/FEATURE-001-dashboard-mvp.md +268 -0
- package/tasks/review/ARCH-007-daemon-monolith.md +162 -0
- package/tasks/review/bmad-review-aegis.md +349 -0
- package/tasks/review/bmad-review-anvil.md +259 -0
- package/tasks/review/bmad-review-crucible.md +277 -0
- package/tasks/review/bmad-review-ember.md +307 -0
- package/tasks/review/bmad-review-furnace.md +285 -0
- package/tasks/review/bmad-review-pixel.md +329 -0
- package/tasks/review/bmad-review-scribe.md +361 -0
- package/tasks/review/bmad-review-sentinel.md +242 -0
- package/tasks/review/task-001.md +78 -0
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dispatch API - Create task files for agent execution
|
|
3
|
+
*
|
|
4
|
+
* This is the key feature that transforms the dashboard from a viewer
|
|
5
|
+
* into a command center. Users can click dispatch buttons to create
|
|
6
|
+
* task files that agents will pick up.
|
|
7
|
+
*
|
|
8
|
+
* Endpoint:
|
|
9
|
+
* POST /api/dispatch - Create and dispatch a task
|
|
10
|
+
*
|
|
11
|
+
* Request body:
|
|
12
|
+
* {
|
|
13
|
+
* "type": "stale-docs", // Issue type
|
|
14
|
+
* "target": "README.md", // Target file/resource
|
|
15
|
+
* "agent": "scribe", // Suggested agent
|
|
16
|
+
* "context": { ... } // Additional context
|
|
17
|
+
* }
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const tasksApi = require('./tasks');
|
|
23
|
+
|
|
24
|
+
// Dispatch type configurations
|
|
25
|
+
const DISPATCH_TYPES = {
|
|
26
|
+
'stale-docs': {
|
|
27
|
+
prefix: 'DOC',
|
|
28
|
+
priority: 'medium',
|
|
29
|
+
suggestedAgent: 'scribe',
|
|
30
|
+
titleTemplate: (target) => `Update stale documentation: ${target}`,
|
|
31
|
+
descriptionTemplate: (target, context) =>
|
|
32
|
+
`The file "${target}" has not been updated in ${context.daysSinceUpdate || 'many'} days and may contain outdated information.`,
|
|
33
|
+
acceptanceCriteria: [
|
|
34
|
+
'Documentation reviewed and updated',
|
|
35
|
+
'All code references are accurate',
|
|
36
|
+
'Examples are current and working'
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
'failing-test': {
|
|
40
|
+
prefix: 'TEST',
|
|
41
|
+
priority: 'high',
|
|
42
|
+
suggestedAgent: 'crucible',
|
|
43
|
+
titleTemplate: (target) => `Fix failing test: ${target}`,
|
|
44
|
+
descriptionTemplate: (target, context) =>
|
|
45
|
+
`Test file "${target}" has ${context.failureCount || 'some'} failing test(s). Error: ${context.error || 'Unknown'}`,
|
|
46
|
+
acceptanceCriteria: [
|
|
47
|
+
'All tests passing',
|
|
48
|
+
'No test skips introduced',
|
|
49
|
+
'Root cause addressed'
|
|
50
|
+
]
|
|
51
|
+
},
|
|
52
|
+
'security-issue': {
|
|
53
|
+
prefix: 'SEC',
|
|
54
|
+
priority: 'critical',
|
|
55
|
+
suggestedAgent: 'aegis',
|
|
56
|
+
titleTemplate: (target) => `Security issue: ${target}`,
|
|
57
|
+
descriptionTemplate: (target, context) =>
|
|
58
|
+
`Security vulnerability detected: ${context.vulnerability || target}. Severity: ${context.severity || 'unknown'}`,
|
|
59
|
+
acceptanceCriteria: [
|
|
60
|
+
'Vulnerability patched',
|
|
61
|
+
'No regressions introduced',
|
|
62
|
+
'Security tests added if applicable'
|
|
63
|
+
]
|
|
64
|
+
},
|
|
65
|
+
'low-coverage': {
|
|
66
|
+
prefix: 'COV',
|
|
67
|
+
priority: 'medium',
|
|
68
|
+
suggestedAgent: 'crucible',
|
|
69
|
+
titleTemplate: (target) => `Improve test coverage: ${target}`,
|
|
70
|
+
descriptionTemplate: (target, context) =>
|
|
71
|
+
`File "${target}" has low test coverage (${context.coverage || '<50'}%). Add tests to improve coverage.`,
|
|
72
|
+
acceptanceCriteria: [
|
|
73
|
+
'Coverage improved to acceptable threshold',
|
|
74
|
+
'Critical paths covered',
|
|
75
|
+
'Edge cases tested'
|
|
76
|
+
]
|
|
77
|
+
},
|
|
78
|
+
'todo-fixme': {
|
|
79
|
+
prefix: 'TODO',
|
|
80
|
+
priority: 'low',
|
|
81
|
+
suggestedAgent: null, // Auto-assign based on file type
|
|
82
|
+
titleTemplate: (target) => `Address TODO/FIXME: ${target}`,
|
|
83
|
+
descriptionTemplate: (target, context) =>
|
|
84
|
+
`TODO/FIXME marker found in "${target}": ${context.marker || 'Review and address'}`,
|
|
85
|
+
acceptanceCriteria: [
|
|
86
|
+
'TODO/FIXME addressed or documented',
|
|
87
|
+
'Code improved as needed',
|
|
88
|
+
'Marker removed if resolved'
|
|
89
|
+
]
|
|
90
|
+
},
|
|
91
|
+
'pending-review': {
|
|
92
|
+
prefix: 'REV',
|
|
93
|
+
priority: 'high',
|
|
94
|
+
suggestedAgent: 'temper',
|
|
95
|
+
titleTemplate: (target) => `Review pending: ${target}`,
|
|
96
|
+
descriptionTemplate: (target, context) =>
|
|
97
|
+
`PR #${context.prNumber || target} has been waiting for review for ${context.hoursWaiting || 'many'} hours.`,
|
|
98
|
+
acceptanceCriteria: [
|
|
99
|
+
'PR reviewed',
|
|
100
|
+
'Feedback provided',
|
|
101
|
+
'Approval or changes requested'
|
|
102
|
+
]
|
|
103
|
+
},
|
|
104
|
+
'custom': {
|
|
105
|
+
prefix: 'TASK',
|
|
106
|
+
priority: 'medium',
|
|
107
|
+
suggestedAgent: null,
|
|
108
|
+
titleTemplate: (target) => target, // Use target as title
|
|
109
|
+
descriptionTemplate: (target, context) =>
|
|
110
|
+
context.description || 'Custom task dispatched from dashboard.',
|
|
111
|
+
acceptanceCriteria: [
|
|
112
|
+
'Task completed as specified'
|
|
113
|
+
]
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Generate unique dispatch task ID
|
|
119
|
+
* @param {string} prefix - ID prefix
|
|
120
|
+
* @returns {string} Task ID
|
|
121
|
+
*/
|
|
122
|
+
function generateDispatchId(prefix) {
|
|
123
|
+
const now = new Date();
|
|
124
|
+
const seq = Math.floor(Math.random() * 1000).toString().padStart(3, '0');
|
|
125
|
+
return `${prefix}-${now.toISOString().slice(0, 10).replace(/-/g, '')}-${seq}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Determine agent based on file type
|
|
130
|
+
* @param {string} target - Target file path
|
|
131
|
+
* @returns {string|null} Suggested agent
|
|
132
|
+
*/
|
|
133
|
+
function suggestAgentForFile(target) {
|
|
134
|
+
if (!target) return null;
|
|
135
|
+
|
|
136
|
+
const ext = path.extname(target).toLowerCase();
|
|
137
|
+
const dir = path.dirname(target).toLowerCase();
|
|
138
|
+
|
|
139
|
+
// Backend files
|
|
140
|
+
if (dir.includes('/api/') || dir.includes('/services/') ||
|
|
141
|
+
dir.includes('/models/') || dir.includes('/middleware/')) {
|
|
142
|
+
return 'furnace';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Frontend files
|
|
146
|
+
if (dir.includes('/components/') || dir.includes('/pages/') ||
|
|
147
|
+
dir.includes('/ui/') || ext === '.tsx' || ext === '.jsx') {
|
|
148
|
+
return 'anvil';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Test files
|
|
152
|
+
if (target.includes('.test.') || target.includes('.spec.') ||
|
|
153
|
+
dir.includes('/test')) {
|
|
154
|
+
return 'crucible';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Documentation
|
|
158
|
+
if (ext === '.md' || dir.includes('/docs/')) {
|
|
159
|
+
return 'scribe';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Security-related
|
|
163
|
+
if (target.includes('auth') || target.includes('security') ||
|
|
164
|
+
target.includes('permission')) {
|
|
165
|
+
return 'aegis';
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Style files
|
|
169
|
+
if (ext === '.css' || ext === '.scss' || ext === '.less') {
|
|
170
|
+
return 'pixel';
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const MAX_TARGET_LENGTH = 500
|
|
177
|
+
const MAX_TITLE_LENGTH = 200
|
|
178
|
+
const MAX_CONTEXT_STRING_LENGTH = 10_000
|
|
179
|
+
const AGENT_ID_PATTERN = /^[a-z0-9_-]+$/
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Validate dispatch request
|
|
183
|
+
* @param {Object} data - Request data
|
|
184
|
+
* @returns {Object} Validation result
|
|
185
|
+
*/
|
|
186
|
+
function validateDispatchRequest(data) {
|
|
187
|
+
const errors = [];
|
|
188
|
+
|
|
189
|
+
if (!data.type) {
|
|
190
|
+
errors.push('type is required');
|
|
191
|
+
} else if (!DISPATCH_TYPES[data.type]) {
|
|
192
|
+
errors.push(`unknown type: ${data.type}. Valid types: ${Object.keys(DISPATCH_TYPES).join(', ')}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (!data.target && data.type !== 'custom') {
|
|
196
|
+
errors.push('target is required');
|
|
197
|
+
} else if (data.target) {
|
|
198
|
+
if (typeof data.target !== 'string') {
|
|
199
|
+
errors.push('target must be a string');
|
|
200
|
+
} else if (data.target.length > MAX_TARGET_LENGTH) {
|
|
201
|
+
errors.push(`target too long (max ${MAX_TARGET_LENGTH} chars)`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (data.title && data.title.length > MAX_TITLE_LENGTH) {
|
|
206
|
+
errors.push(`title too long (max ${MAX_TITLE_LENGTH} chars)`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (data.agent) {
|
|
210
|
+
if (typeof data.agent !== 'string') {
|
|
211
|
+
errors.push('agent must be a string');
|
|
212
|
+
} else if (!AGENT_ID_PATTERN.test(data.agent)) {
|
|
213
|
+
errors.push('agent must contain only lowercase letters, numbers, underscores, and hyphens');
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (data.context) {
|
|
218
|
+
if (typeof data.context !== 'object' || Array.isArray(data.context)) {
|
|
219
|
+
errors.push('context must be an object');
|
|
220
|
+
} else {
|
|
221
|
+
// Limit total serialized context size to prevent oversized payloads
|
|
222
|
+
const contextSize = JSON.stringify(data.context).length;
|
|
223
|
+
if (contextSize > MAX_CONTEXT_STRING_LENGTH) {
|
|
224
|
+
errors.push(`context too large (${contextSize} chars serialized, max ${MAX_CONTEXT_STRING_LENGTH})`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
valid: errors.length === 0,
|
|
231
|
+
errors
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Dispatch a task
|
|
237
|
+
* @param {string} projectRoot - Project root directory
|
|
238
|
+
* @param {Object} data - Dispatch request data
|
|
239
|
+
* @param {Function} broadcast - WebSocket broadcast function
|
|
240
|
+
* @returns {Object} Created task
|
|
241
|
+
*/
|
|
242
|
+
// Sentinel error type so the server can distinguish validation (400) from runtime (500)
|
|
243
|
+
class DispatchValidationError extends Error {
|
|
244
|
+
constructor(errors) {
|
|
245
|
+
super(`Invalid dispatch request: ${errors.join(', ')}`)
|
|
246
|
+
this.name = 'DispatchValidationError'
|
|
247
|
+
this.errors = errors
|
|
248
|
+
this.statusCode = 400
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function dispatch(projectRoot, data, broadcast) {
|
|
253
|
+
// Validate request — throws DispatchValidationError (400) on failure
|
|
254
|
+
const validation = validateDispatchRequest(data);
|
|
255
|
+
if (!validation.valid) {
|
|
256
|
+
throw new DispatchValidationError(validation.errors);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const dispatchType = DISPATCH_TYPES[data.type];
|
|
260
|
+
|
|
261
|
+
// Sanitize context strings: strip any markdown frontmatter (prevents injecting fake task metadata)
|
|
262
|
+
const rawContext = data.context || {};
|
|
263
|
+
const context = Object.fromEntries(
|
|
264
|
+
Object.entries(rawContext).map(([k, v]) => [
|
|
265
|
+
k,
|
|
266
|
+
typeof v === 'string' ? v.replace(/^---[\s\S]*?---\n?/, '').trim() : v
|
|
267
|
+
])
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
// Determine agent
|
|
271
|
+
let agent = data.agent || dispatchType.suggestedAgent;
|
|
272
|
+
if (!agent) {
|
|
273
|
+
agent = suggestAgentForFile(data.target);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Generate task ID
|
|
277
|
+
const taskId = generateDispatchId(dispatchType.prefix);
|
|
278
|
+
|
|
279
|
+
// Build task title and description
|
|
280
|
+
const title = data.title || dispatchType.titleTemplate(data.target);
|
|
281
|
+
const description = dispatchType.descriptionTemplate(data.target, context);
|
|
282
|
+
|
|
283
|
+
// Build acceptance criteria
|
|
284
|
+
const criteria = data.acceptanceCriteria || dispatchType.acceptanceCriteria;
|
|
285
|
+
|
|
286
|
+
// Create task content
|
|
287
|
+
const taskData = {
|
|
288
|
+
id: taskId,
|
|
289
|
+
title: title,
|
|
290
|
+
type: mapDispatchTypeToTaskType(data.type),
|
|
291
|
+
priority: data.priority || dispatchType.priority,
|
|
292
|
+
assignedTo: agent,
|
|
293
|
+
createdBy: 'dashboard-dispatch',
|
|
294
|
+
summary: description,
|
|
295
|
+
context: buildContextSection(data, context)
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
// Build full task file content
|
|
299
|
+
const tasksDir = path.join(projectRoot, 'tasks', 'pending');
|
|
300
|
+
const slug = title
|
|
301
|
+
.toLowerCase()
|
|
302
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
303
|
+
.replace(/^-|-$/g, '')
|
|
304
|
+
.substring(0, 50);
|
|
305
|
+
|
|
306
|
+
const filename = `${taskId}-${slug}.md`;
|
|
307
|
+
const filePath = path.join(tasksDir, filename);
|
|
308
|
+
|
|
309
|
+
const now = new Date().toISOString();
|
|
310
|
+
const content = `---
|
|
311
|
+
id: ${taskId}
|
|
312
|
+
title: "${title.replace(/"/g, '\\"')}"
|
|
313
|
+
type: ${taskData.type}
|
|
314
|
+
priority: ${taskData.priority}
|
|
315
|
+
status: pending
|
|
316
|
+
created_at: ${now}
|
|
317
|
+
created_by: dashboard-dispatch
|
|
318
|
+
assigned_to: ${agent || 'null'}
|
|
319
|
+
dispatch_type: ${data.type}
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
# ${title}
|
|
323
|
+
|
|
324
|
+
## Summary
|
|
325
|
+
|
|
326
|
+
${description}
|
|
327
|
+
|
|
328
|
+
## Context
|
|
329
|
+
|
|
330
|
+
${buildContextSection(data, context)}
|
|
331
|
+
|
|
332
|
+
## Target
|
|
333
|
+
|
|
334
|
+
${data.target ? `- File/Resource: \`${data.target}\`` : '- No specific target'}
|
|
335
|
+
|
|
336
|
+
## Acceptance Criteria
|
|
337
|
+
|
|
338
|
+
${criteria.map(c => `- [ ] ${c}`).join('\n')}
|
|
339
|
+
|
|
340
|
+
## Dispatch Info
|
|
341
|
+
|
|
342
|
+
- **Dispatched via:** Dashboard Web UI
|
|
343
|
+
- **Dispatch type:** ${data.type}
|
|
344
|
+
- **Suggested agent:** ${agent || 'auto-assign'}
|
|
345
|
+
- **Timestamp:** ${now}
|
|
346
|
+
`;
|
|
347
|
+
|
|
348
|
+
// Ensure directory exists
|
|
349
|
+
if (!fs.existsSync(tasksDir)) {
|
|
350
|
+
fs.mkdirSync(tasksDir, { recursive: true });
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Write task file
|
|
354
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
355
|
+
|
|
356
|
+
console.log(`[Dispatch] Created task: ${filename}`);
|
|
357
|
+
|
|
358
|
+
// Read back the created task
|
|
359
|
+
const createdTask = tasksApi.readTaskFile(filePath);
|
|
360
|
+
|
|
361
|
+
// Broadcast task creation
|
|
362
|
+
if (broadcast) {
|
|
363
|
+
broadcast({
|
|
364
|
+
type: 'task-created',
|
|
365
|
+
task: createdTask,
|
|
366
|
+
dispatch: {
|
|
367
|
+
type: data.type,
|
|
368
|
+
target: data.target,
|
|
369
|
+
agent: agent
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
success: true,
|
|
376
|
+
task: createdTask,
|
|
377
|
+
file: filename,
|
|
378
|
+
dispatchType: data.type,
|
|
379
|
+
suggestedAgent: agent
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Map dispatch type to task type
|
|
385
|
+
* @param {string} dispatchType - Dispatch type
|
|
386
|
+
* @returns {string} Task type
|
|
387
|
+
*/
|
|
388
|
+
function mapDispatchTypeToTaskType(dispatchType) {
|
|
389
|
+
const typeMap = {
|
|
390
|
+
'stale-docs': 'documentation',
|
|
391
|
+
'failing-test': 'bug',
|
|
392
|
+
'security-issue': 'security',
|
|
393
|
+
'low-coverage': 'test',
|
|
394
|
+
'todo-fixme': 'improvement',
|
|
395
|
+
'pending-review': 'review',
|
|
396
|
+
'custom': 'task'
|
|
397
|
+
};
|
|
398
|
+
return typeMap[dispatchType] || 'task';
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Build context section for task
|
|
403
|
+
* @param {Object} data - Dispatch data
|
|
404
|
+
* @param {Object} context - Additional context
|
|
405
|
+
* @returns {string} Context section content
|
|
406
|
+
*/
|
|
407
|
+
function buildContextSection(data, context) {
|
|
408
|
+
const lines = [];
|
|
409
|
+
|
|
410
|
+
if (data.type === 'stale-docs') {
|
|
411
|
+
lines.push(`This documentation has not been updated recently.`);
|
|
412
|
+
if (context.lastUpdated) {
|
|
413
|
+
lines.push(`Last updated: ${context.lastUpdated}`);
|
|
414
|
+
}
|
|
415
|
+
if (context.daysSinceUpdate) {
|
|
416
|
+
lines.push(`Days since update: ${context.daysSinceUpdate}`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (data.type === 'failing-test') {
|
|
421
|
+
lines.push(`Test failure detected during automated testing.`);
|
|
422
|
+
if (context.error) {
|
|
423
|
+
lines.push(`\nError message:\n\`\`\`\n${context.error}\n\`\`\``);
|
|
424
|
+
}
|
|
425
|
+
if (context.failureCount) {
|
|
426
|
+
lines.push(`Failure count: ${context.failureCount}`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (data.type === 'security-issue') {
|
|
431
|
+
lines.push(`Security issue detected.`);
|
|
432
|
+
if (context.vulnerability) {
|
|
433
|
+
lines.push(`Vulnerability: ${context.vulnerability}`);
|
|
434
|
+
}
|
|
435
|
+
if (context.severity) {
|
|
436
|
+
lines.push(`Severity: ${context.severity}`);
|
|
437
|
+
}
|
|
438
|
+
if (context.cve) {
|
|
439
|
+
lines.push(`CVE: ${context.cve}`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (data.type === 'low-coverage') {
|
|
444
|
+
lines.push(`Code coverage is below acceptable threshold.`);
|
|
445
|
+
if (context.coverage) {
|
|
446
|
+
lines.push(`Current coverage: ${context.coverage}%`);
|
|
447
|
+
}
|
|
448
|
+
if (context.threshold) {
|
|
449
|
+
lines.push(`Required threshold: ${context.threshold}%`);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (context.reason) {
|
|
454
|
+
lines.push(`\nReason: ${context.reason}`);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (context.notes) {
|
|
458
|
+
lines.push(`\nNotes: ${context.notes}`);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return lines.length > 0 ? lines.join('\n') : 'Dispatched from dashboard.';
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Get available dispatch types
|
|
466
|
+
* @returns {Object} Dispatch type configurations
|
|
467
|
+
*/
|
|
468
|
+
function getDispatchTypes() {
|
|
469
|
+
return Object.entries(DISPATCH_TYPES).map(([key, config]) => ({
|
|
470
|
+
type: key,
|
|
471
|
+
prefix: config.prefix,
|
|
472
|
+
priority: config.priority,
|
|
473
|
+
suggestedAgent: config.suggestedAgent
|
|
474
|
+
}));
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
module.exports = {
|
|
478
|
+
dispatch,
|
|
479
|
+
getDispatchTypes,
|
|
480
|
+
validateDispatchRequest,
|
|
481
|
+
DispatchValidationError,
|
|
482
|
+
DISPATCH_TYPES
|
|
483
|
+
};
|