vibe-forge 0.4.0 → 0.8.2

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