@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.
Files changed (201) hide show
  1. package/.claude/commands/clear-attention.md +63 -0
  2. package/.claude/commands/compact-context.md +52 -0
  3. package/.claude/commands/configure-vcs.md +102 -0
  4. package/.claude/commands/forge.md +171 -0
  5. package/.claude/commands/need-help.md +77 -0
  6. package/.claude/commands/update-status.md +64 -0
  7. package/.claude/commands/worker-loop.md +106 -0
  8. package/.claude/hooks/worker-loop.js +198 -0
  9. package/.claude/scripts/setup-worker-loop.sh +45 -0
  10. package/.claude/settings.local.json +46 -0
  11. package/LICENSE +21 -0
  12. package/README.md +238 -0
  13. package/agents/aegis/personality.md +294 -0
  14. package/agents/anvil/personality.md +276 -0
  15. package/agents/architect/personality.md +258 -0
  16. package/agents/crucible/personality.md +360 -0
  17. package/agents/ember/personality.md +291 -0
  18. package/agents/forge-master/capabilities.md +144 -0
  19. package/agents/forge-master/context-template.md +128 -0
  20. package/agents/forge-master/personality.md +138 -0
  21. package/agents/furnace/personality.md +340 -0
  22. package/agents/herald/personality.md +247 -0
  23. package/agents/loki/personality.md +108 -0
  24. package/agents/oracle/personality.md +283 -0
  25. package/agents/pixel/personality.md +113 -0
  26. package/agents/planning-hub/personality.md +320 -0
  27. package/agents/scribe/personality.md +251 -0
  28. package/agents/temper/personality.md +218 -0
  29. package/bin/cli.js +375 -0
  30. package/bin/dashboard/api/agents.js +333 -0
  31. package/bin/dashboard/api/dispatch.js +483 -0
  32. package/bin/dashboard/api/tasks.js +416 -0
  33. package/bin/dashboard/frontend/index.html +13 -0
  34. package/bin/dashboard/frontend/package.json +16 -0
  35. package/bin/dashboard/frontend/src/App.svelte +222 -0
  36. package/bin/dashboard/frontend/src/app.css +1777 -0
  37. package/bin/dashboard/frontend/src/lib/components/AgentCard.svelte +60 -0
  38. package/bin/dashboard/frontend/src/lib/components/AgentsPanel.svelte +57 -0
  39. package/bin/dashboard/frontend/src/lib/components/DispatchModal.svelte +180 -0
  40. package/bin/dashboard/frontend/src/lib/components/Footer.svelte +33 -0
  41. package/bin/dashboard/frontend/src/lib/components/Header.svelte +84 -0
  42. package/bin/dashboard/frontend/src/lib/components/IssueCard.svelte +33 -0
  43. package/bin/dashboard/frontend/src/lib/components/IssuesPanel.svelte +73 -0
  44. package/bin/dashboard/frontend/src/lib/components/KeyboardShortcutsModal.svelte +108 -0
  45. package/bin/dashboard/frontend/src/lib/components/MobileTabs.svelte +52 -0
  46. package/bin/dashboard/frontend/src/lib/components/NotificationCard.svelte +60 -0
  47. package/bin/dashboard/frontend/src/lib/components/NotificationsPanel.svelte +44 -0
  48. package/bin/dashboard/frontend/src/lib/components/TaskCard.svelte +63 -0
  49. package/bin/dashboard/frontend/src/lib/components/TasksPanel.svelte +82 -0
  50. package/bin/dashboard/frontend/src/lib/components/Toast.svelte +45 -0
  51. package/bin/dashboard/frontend/src/lib/stores/agents.js +34 -0
  52. package/bin/dashboard/frontend/src/lib/stores/issues.js +54 -0
  53. package/bin/dashboard/frontend/src/lib/stores/notifications.js +48 -0
  54. package/bin/dashboard/frontend/src/lib/stores/tasks.js +63 -0
  55. package/bin/dashboard/frontend/src/lib/stores/theme.js +33 -0
  56. package/bin/dashboard/frontend/src/lib/stores/toast.js +35 -0
  57. package/bin/dashboard/frontend/src/lib/stores/ui.js +25 -0
  58. package/bin/dashboard/frontend/src/lib/stores/voice.js +275 -0
  59. package/bin/dashboard/frontend/src/lib/stores/websocket.js +295 -0
  60. package/bin/dashboard/frontend/src/lib/utils/api.js +101 -0
  61. package/bin/dashboard/frontend/src/lib/utils/formatters.js +54 -0
  62. package/bin/dashboard/frontend/src/main.js +9 -0
  63. package/bin/dashboard/frontend/svelte.config.js +5 -0
  64. package/bin/dashboard/frontend/vite.config.js +20 -0
  65. package/bin/dashboard/public/assets/index-DnfVj9Ce.css +1 -0
  66. package/bin/dashboard/public/assets/index-Ze5h0kXQ.js +2 -0
  67. package/bin/dashboard/public/index.html +14 -0
  68. package/bin/dashboard/server.js +566 -0
  69. package/bin/forge-daemon.sh +463 -0
  70. package/bin/forge-setup.sh +645 -0
  71. package/bin/forge-spawn.sh +164 -0
  72. package/bin/forge.cmd +83 -0
  73. package/bin/forge.sh +533 -0
  74. package/bin/lib/agents.sh +177 -0
  75. package/bin/lib/colors.sh +44 -0
  76. package/bin/lib/config.sh +347 -0
  77. package/bin/lib/constants.sh +241 -0
  78. package/bin/lib/daemon/display.sh +128 -0
  79. package/bin/lib/daemon/notifications.sh +263 -0
  80. package/bin/lib/daemon/routing.sh +77 -0
  81. package/bin/lib/daemon/state.sh +115 -0
  82. package/bin/lib/daemon/sync.sh +95 -0
  83. package/bin/lib/database.sh +310 -0
  84. package/bin/lib/heimdall-setup.js +113 -0
  85. package/bin/lib/heimdall.js +265 -0
  86. package/bin/lib/json.sh +264 -0
  87. package/bin/lib/terminal.js +451 -0
  88. package/bin/lib/util.sh +126 -0
  89. package/bin/lib/vcs.js +349 -0
  90. package/config/agent-manifest.yaml +203 -0
  91. package/config/agents.json +168 -0
  92. package/config/task-template.md +159 -0
  93. package/config/task-types.yaml +106 -0
  94. package/context/agent-status/aegis.json +7 -0
  95. package/context/agent-status/anvil.json +7 -0
  96. package/context/agent-status/architect.json +7 -0
  97. package/context/agent-status/crucible.json +7 -0
  98. package/context/agent-status/ember.json +7 -0
  99. package/context/agent-status/furnace.json +7 -0
  100. package/context/agent-status/loki.json +7 -0
  101. package/context/agent-status/oracle.json +7 -0
  102. package/context/agent-status/pixel.json +7 -0
  103. package/context/agent-status/planning-hub.json +7 -0
  104. package/context/agent-status/scribe.json +7 -0
  105. package/context/agent-status/temper.json +7 -0
  106. package/context/feature-brainstorm.md +426 -0
  107. package/context/forge-state.yaml +19 -0
  108. package/context/modern-conventions.md +129 -0
  109. package/context/project-context-template.md +122 -0
  110. package/context/project-context.md +122 -0
  111. package/docs/TODO.md +150 -0
  112. package/docs/agents.md +409 -0
  113. package/docs/architecture/decisions/ADR-001-daemon-modularization.md +122 -0
  114. package/docs/architecture/vibe-lab-integration.md +684 -0
  115. package/docs/architecture.md +194 -0
  116. package/docs/bmad-gap-analysis-2026-03-31.md +444 -0
  117. package/docs/cleanup-workflow.md +329 -0
  118. package/docs/commands.md +451 -0
  119. package/docs/dashboard-mockup.html +989 -0
  120. package/docs/getting-started.md +261 -0
  121. package/docs/integration/forge-ownership-policy.md +112 -0
  122. package/docs/npm-publishing.md +132 -0
  123. package/docs/roadmap-2026.md +519 -0
  124. package/docs/security.md +144 -0
  125. package/docs/wireframes/dashboard-mvp.md +1164 -0
  126. package/docs/workflows/README.md +32 -0
  127. package/docs/workflows/azure-devops.md +108 -0
  128. package/docs/workflows/bitbucket.md +104 -0
  129. package/docs/workflows/git-only.md +130 -0
  130. package/docs/workflows/gitea.md +168 -0
  131. package/docs/workflows/github.md +103 -0
  132. package/docs/workflows/gitlab.md +105 -0
  133. package/docs/workflows.md +454 -0
  134. package/package.json +73 -0
  135. package/tasks/completed/ARCH-001-duplicate-agent-config.md +121 -0
  136. package/tasks/completed/ARCH-002-mixed-bash-node-implementation.md +88 -0
  137. package/tasks/completed/ARCH-003-worker-loop-hook-duplication.md +77 -0
  138. package/tasks/completed/ARCH-009-test-organization.md +78 -0
  139. package/tasks/completed/ARCH-011-jq-vs-nodejs-json.md +94 -0
  140. package/tasks/completed/ARCH-012-tmp-files-in-root.md +71 -0
  141. package/tasks/completed/ARCH-013-exit-code-constants.md +65 -0
  142. package/tasks/completed/ARCH-014-sed-incompatibility.md +96 -0
  143. package/tasks/completed/ARCH-015-docs-todo-tracking.md +83 -0
  144. package/tasks/completed/BUG-dash-001-tasks-filter-error.md +31 -0
  145. package/tasks/completed/BUG-dash-002-agents-unknown.md +41 -0
  146. package/tasks/completed/CLEAN-001.md +38 -0
  147. package/tasks/completed/CLEAN-002.md +43 -0
  148. package/tasks/completed/CLEAN-003.md +47 -0
  149. package/tasks/completed/CLEAN-004.md +56 -0
  150. package/tasks/completed/CLEAN-005.md +75 -0
  151. package/tasks/completed/CLEAN-006.md +47 -0
  152. package/tasks/completed/CLEAN-007.md +34 -0
  153. package/tasks/completed/CLEAN-008.md +49 -0
  154. package/tasks/completed/CLEAN-012.md +58 -0
  155. package/tasks/completed/CLEAN-013.md +45 -0
  156. package/tasks/completed/FEATURE-001a-dashboard-wireframes.md +162 -0
  157. package/tasks/completed/IMPL-007a-daemon-notifications-module.md +82 -0
  158. package/tasks/completed/IMPL-007b-daemon-sync-module.md +71 -0
  159. package/tasks/completed/IMPL-007c-daemon-state-module.md +80 -0
  160. package/tasks/completed/IMPL-007d-daemon-routing-module.md +77 -0
  161. package/tasks/completed/IMPL-007e-daemon-display-module.md +77 -0
  162. package/tasks/completed/IMPL-007f-daemon-integration.md +124 -0
  163. package/tasks/completed/PLAT-1-heimdall.md +420 -0
  164. package/tasks/completed/SEC-001-sql-injection-fix.md +58 -0
  165. package/tasks/completed/SEC-002-notification-injection-fix.md +45 -0
  166. package/tasks/completed/SEC-003-eval-injection-fix.md +54 -0
  167. package/tasks/completed/SEC-004-pid-race-condition-fix.md +49 -0
  168. package/tasks/completed/SEC-005-worker-loop-path-fix.md +51 -0
  169. package/tasks/completed/SEC-006-eval-agent-names.md +55 -0
  170. package/tasks/completed/SEC-007-spawn-escaping.md +67 -0
  171. package/tasks/completed/TASK-DASH-001-server-infrastructure.md +185 -0
  172. package/tasks/completed/TASK-anvil-001-dashboard-frontend.md +133 -0
  173. package/tasks/completed/review-bmad-aegis.md +89 -0
  174. package/tasks/completed/review-bmad-anvil.md +80 -0
  175. package/tasks/completed/review-bmad-crucible.md +81 -0
  176. package/tasks/completed/review-bmad-ember.md +90 -0
  177. package/tasks/completed/review-bmad-furnace.md +79 -0
  178. package/tasks/completed/review-bmad-pixel.md +82 -0
  179. package/tasks/completed/review-bmad-scribe.md +92 -0
  180. package/tasks/completed/review-bmad-sentinel.md +83 -0
  181. package/tasks/pending/ARCH-004-git-bash-detection-duplication.md +72 -0
  182. package/tasks/pending/ARCH-005-missing-src-directory.md +95 -0
  183. package/tasks/pending/ARCH-006-task-template-location.md +64 -0
  184. package/tasks/pending/ARCH-008-forge-master-vs-hub.md +81 -0
  185. package/tasks/pending/ARCH-010-missing-index-files.md +84 -0
  186. package/tasks/pending/CLEAN-009.md +31 -0
  187. package/tasks/pending/CLEAN-010.md +30 -0
  188. package/tasks/pending/CLEAN-011.md +30 -0
  189. package/tasks/pending/CLEAN-014.md +32 -0
  190. package/tasks/pending/DESIGN-dash-001-layout-review.md +45 -0
  191. package/tasks/pending/FEATURE-001-dashboard-mvp.md +268 -0
  192. package/tasks/review/ARCH-007-daemon-monolith.md +162 -0
  193. package/tasks/review/bmad-review-aegis.md +349 -0
  194. package/tasks/review/bmad-review-anvil.md +259 -0
  195. package/tasks/review/bmad-review-crucible.md +277 -0
  196. package/tasks/review/bmad-review-ember.md +307 -0
  197. package/tasks/review/bmad-review-furnace.md +285 -0
  198. package/tasks/review/bmad-review-pixel.md +329 -0
  199. package/tasks/review/bmad-review-scribe.md +361 -0
  200. package/tasks/review/bmad-review-sentinel.md +242 -0
  201. package/tasks/review/task-001.md +78 -0
@@ -0,0 +1,416 @@
1
+ /**
2
+ * Tasks API - List and manage tasks from tasks/ directory
3
+ *
4
+ * Endpoints:
5
+ * GET /api/tasks - List all tasks grouped by status
6
+ * GET /api/tasks/:id - Get single task details
7
+ * POST /api/tasks - Create new task
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+
13
+ // Task directory structure
14
+ const TASK_DIRS = ['pending', 'in-progress', 'review', 'completed'];
15
+
16
+ // =============================================================================
17
+ // Task List Cache
18
+ // =============================================================================
19
+ // Avoids a full filesystem scan on every GET /api/tasks request.
20
+ // Invalidation strategy: track each task directory's mtime. When any directory
21
+ // mtime changes (file added, removed, or renamed), the cache is invalidated
22
+ // and rebuilt on the next request. This is O(TASK_DIRS) stat calls vs. O(N)
23
+ // file reads per request, and is the same pattern used by the daemon's
24
+ // sync_agent_status_to_db (mtime filtering before DB upsert).
25
+
26
+ /** @type {Object|null} Cached listTasks result */
27
+ let _taskCache = null;
28
+
29
+ /** @type {Object.<string, number|null>} Last-seen mtime per task dir */
30
+ let _taskCacheMtimes = {};
31
+
32
+ /**
33
+ * Returns true if all task directory mtimes match the recorded values.
34
+ * A null entry means the directory was absent when last recorded.
35
+ * @param {string} tasksDir - Absolute path to tasks/ root
36
+ * @returns {boolean}
37
+ */
38
+ function isTaskCacheValid(tasksDir) {
39
+ if (!_taskCache) return false;
40
+ for (const dir of TASK_DIRS) {
41
+ const dirPath = path.join(tasksDir, dir);
42
+ try {
43
+ const currentMtime = fs.statSync(dirPath).mtimeMs;
44
+ if (currentMtime !== _taskCacheMtimes[dir]) return false;
45
+ } catch (_err) {
46
+ // Directory does not exist — invalid if we previously recorded it
47
+ if (_taskCacheMtimes[dir] !== null) return false;
48
+ }
49
+ }
50
+ return true;
51
+ }
52
+
53
+ /**
54
+ * Snapshots the current mtime of each task directory.
55
+ * Called after a successful scan to anchor the cache.
56
+ * @param {string} tasksDir - Absolute path to tasks/ root
57
+ */
58
+ function recordTaskCacheMtimes(tasksDir) {
59
+ for (const dir of TASK_DIRS) {
60
+ const dirPath = path.join(tasksDir, dir);
61
+ try {
62
+ _taskCacheMtimes[dir] = fs.statSync(dirPath).mtimeMs;
63
+ } catch (_err) {
64
+ _taskCacheMtimes[dir] = null;
65
+ }
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Manually invalidate the task cache (e.g. after createTask).
71
+ * The next listTasks call will trigger a full rescan.
72
+ */
73
+ function invalidateTaskCache() {
74
+ _taskCache = null;
75
+ _taskCacheMtimes = {};
76
+ }
77
+
78
+ /**
79
+ * Parse YAML-like frontmatter from task file
80
+ * @param {string} content - File content
81
+ * @returns {Object} Parsed frontmatter and body
82
+ */
83
+ function parseFrontmatter(content) {
84
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
85
+
86
+ if (!frontmatterMatch) {
87
+ return { metadata: {}, body: content };
88
+ }
89
+
90
+ const frontmatter = frontmatterMatch[1];
91
+ const body = frontmatterMatch[2];
92
+
93
+ // Parse YAML-like frontmatter (simple key: value format)
94
+ const metadata = {};
95
+ const lines = frontmatter.split('\n');
96
+
97
+ for (const line of lines) {
98
+ const match = line.match(/^([a-z_-]+):\s*(.*)$/i);
99
+ if (match) {
100
+ let value = match[2].trim();
101
+ // Remove quotes if present
102
+ if ((value.startsWith('"') && value.endsWith('"')) ||
103
+ (value.startsWith("'") && value.endsWith("'"))) {
104
+ value = value.slice(1, -1);
105
+ }
106
+ // Handle null
107
+ if (value === 'null') {
108
+ value = null;
109
+ }
110
+ metadata[match[1]] = value;
111
+ }
112
+ }
113
+
114
+ return { metadata, body };
115
+ }
116
+
117
+ /**
118
+ * Read a single task file
119
+ * @param {string} filePath - Path to task file
120
+ * @returns {Object|null} Task object or null if invalid
121
+ */
122
+ function readTaskFile(filePath) {
123
+ try {
124
+ const content = fs.readFileSync(filePath, 'utf8');
125
+ const { metadata, body } = parseFrontmatter(content);
126
+
127
+ // Extract title from body if not in metadata
128
+ if (!metadata.title) {
129
+ const titleMatch = body.match(/^#\s+(.+)$/m);
130
+ if (titleMatch) {
131
+ metadata.title = titleMatch[1];
132
+ }
133
+ }
134
+
135
+ // Get file stats for timestamps
136
+ const stats = fs.statSync(filePath);
137
+
138
+ return {
139
+ id: metadata.id || path.basename(filePath, '.md'),
140
+ title: metadata.title || path.basename(filePath, '.md'),
141
+ type: metadata.type || 'task',
142
+ priority: metadata.priority || 'medium',
143
+ status: metadata.status || 'unknown',
144
+ assignedTo: metadata.assigned_to || null,
145
+ createdAt: metadata.created_at || stats.birthtime.toISOString(),
146
+ createdBy: metadata.created_by || 'unknown',
147
+ parent: metadata.parent || null,
148
+ file: path.basename(filePath),
149
+ filePath: filePath,
150
+ modifiedAt: stats.mtime.toISOString()
151
+ };
152
+ } catch (err) {
153
+ console.error(`[Tasks] Error reading ${filePath}: ${err.message}`);
154
+ return null;
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Get task details including full body
160
+ * @param {string} filePath - Path to task file
161
+ * @returns {Object|null} Full task object or null
162
+ */
163
+ function readTaskFull(filePath) {
164
+ try {
165
+ const content = fs.readFileSync(filePath, 'utf8');
166
+ const { metadata, body } = parseFrontmatter(content);
167
+ const stats = fs.statSync(filePath);
168
+
169
+ return {
170
+ id: metadata.id || path.basename(filePath, '.md'),
171
+ title: metadata.title || path.basename(filePath, '.md'),
172
+ type: metadata.type || 'task',
173
+ priority: metadata.priority || 'medium',
174
+ status: metadata.status || 'unknown',
175
+ assignedTo: metadata.assigned_to || null,
176
+ createdAt: metadata.created_at || stats.birthtime.toISOString(),
177
+ createdBy: metadata.created_by || 'unknown',
178
+ parent: metadata.parent || null,
179
+ file: path.basename(filePath),
180
+ filePath: filePath,
181
+ modifiedAt: stats.mtime.toISOString(),
182
+ body: body.trim(),
183
+ raw: content
184
+ };
185
+ } catch (err) {
186
+ console.error(`[Tasks] Error reading full ${filePath}: ${err.message}`);
187
+ return null;
188
+ }
189
+ }
190
+
191
+ /**
192
+ * List all tasks grouped by status.
193
+ * Results are cached and only rescanned when a task directory's mtime changes
194
+ * (i.e. a file was added, removed, or renamed). Individual file edits that
195
+ * don't change the directory mtime will not invalidate the list cache — use
196
+ * GET /api/tasks/:id for live file content.
197
+ *
198
+ * @param {string} projectRoot - Project root directory
199
+ * @returns {Object} Tasks grouped by status
200
+ */
201
+ async function listTasks(projectRoot) {
202
+ const tasksDir = path.join(projectRoot, 'tasks');
203
+
204
+ // Return cached result when no directory has changed
205
+ if (isTaskCacheValid(tasksDir)) {
206
+ return _taskCache;
207
+ }
208
+
209
+ const result = {
210
+ pending: [],
211
+ 'in-progress': [],
212
+ review: [],
213
+ completed: [],
214
+ summary: {
215
+ total: 0,
216
+ pending: 0,
217
+ inProgress: 0,
218
+ review: 0,
219
+ completed: 0
220
+ }
221
+ };
222
+
223
+ for (const dir of TASK_DIRS) {
224
+ const dirPath = path.join(tasksDir, dir);
225
+
226
+ if (!fs.existsSync(dirPath)) {
227
+ continue;
228
+ }
229
+
230
+ const files = fs.readdirSync(dirPath).filter(f => f.endsWith('.md'));
231
+
232
+ for (const file of files) {
233
+ const filePath = path.join(dirPath, file);
234
+ const task = readTaskFile(filePath);
235
+
236
+ if (task) {
237
+ result[dir].push(task);
238
+ result.summary.total++;
239
+
240
+ switch (dir) {
241
+ case 'pending':
242
+ result.summary.pending++;
243
+ break;
244
+ case 'in-progress':
245
+ result.summary.inProgress++;
246
+ break;
247
+ case 'review':
248
+ result.summary.review++;
249
+ break;
250
+ case 'completed':
251
+ result.summary.completed++;
252
+ break;
253
+ }
254
+ }
255
+ }
256
+
257
+ // Sort by priority (critical > high > medium > low)
258
+ const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
259
+ result[dir].sort((a, b) => {
260
+ const pa = priorityOrder[a.priority] ?? 2;
261
+ const pb = priorityOrder[b.priority] ?? 2;
262
+ return pa - pb;
263
+ });
264
+ }
265
+
266
+ // Store result and anchor mtimes for next validation
267
+ recordTaskCacheMtimes(tasksDir);
268
+ _taskCache = result;
269
+
270
+ return result;
271
+ }
272
+
273
+ /**
274
+ * Get a single task by ID
275
+ * @param {string} projectRoot - Project root directory
276
+ * @param {string} taskId - Task ID to find
277
+ * @returns {Object|null} Task object or null
278
+ */
279
+ async function getTask(projectRoot, taskId) {
280
+ const tasksDir = path.join(projectRoot, 'tasks');
281
+
282
+ // Sanitize task ID to prevent path traversal
283
+ const safeId = taskId.replace(/[^a-zA-Z0-9_-]/g, '');
284
+
285
+ for (const dir of TASK_DIRS) {
286
+ const dirPath = path.join(tasksDir, dir);
287
+
288
+ if (!fs.existsSync(dirPath)) {
289
+ continue;
290
+ }
291
+
292
+ const files = fs.readdirSync(dirPath).filter(f => f.endsWith('.md'));
293
+
294
+ for (const file of files) {
295
+ // Check if filename starts with task ID
296
+ if (file.startsWith(safeId) || file === `${safeId}.md`) {
297
+ const filePath = path.join(dirPath, file);
298
+ return readTaskFull(filePath);
299
+ }
300
+
301
+ // Also check file content for matching ID
302
+ const task = readTaskFile(path.join(dirPath, file));
303
+ if (task && task.id === safeId) {
304
+ return readTaskFull(path.join(dirPath, file));
305
+ }
306
+ }
307
+ }
308
+
309
+ return null;
310
+ }
311
+
312
+ /**
313
+ * Create a new task file
314
+ * @param {string} projectRoot - Project root directory
315
+ * @param {Object} taskData - Task data
316
+ * @returns {Object} Created task
317
+ */
318
+ async function createTask(projectRoot, taskData) {
319
+ const tasksDir = path.join(projectRoot, 'tasks', 'pending');
320
+
321
+ // Validate required fields
322
+ if (!taskData.title) {
323
+ throw new Error('Task title is required');
324
+ }
325
+
326
+ // Generate task ID if not provided
327
+ const id = taskData.id || generateTaskId(taskData.type || 'TASK');
328
+
329
+ // Sanitize ID
330
+ const safeId = id.replace(/[^a-zA-Z0-9_-]/g, '');
331
+
332
+ // Generate filename
333
+ const slug = taskData.title
334
+ .toLowerCase()
335
+ .replace(/[^a-z0-9]+/g, '-')
336
+ .replace(/^-|-$/g, '')
337
+ .substring(0, 50);
338
+
339
+ const filename = `${safeId}-${slug}.md`;
340
+ const filePath = path.join(tasksDir, filename);
341
+
342
+ // Check if file already exists
343
+ if (fs.existsSync(filePath)) {
344
+ throw new Error(`Task file already exists: ${filename}`);
345
+ }
346
+
347
+ // Build task content
348
+ const now = new Date().toISOString();
349
+ const content = `---
350
+ id: ${safeId}
351
+ title: "${taskData.title.replace(/"/g, '\\"')}"
352
+ type: ${taskData.type || 'task'}
353
+ priority: ${taskData.priority || 'medium'}
354
+ status: pending
355
+ created_at: ${now}
356
+ created_by: ${taskData.createdBy || 'dashboard'}
357
+ assigned_to: ${taskData.assignedTo || 'null'}
358
+ ---
359
+
360
+ # ${taskData.title}
361
+
362
+ ## Summary
363
+
364
+ ${taskData.summary || taskData.description || 'No description provided.'}
365
+
366
+ ## Context
367
+
368
+ ${taskData.context || 'Created via dashboard dispatch.'}
369
+
370
+ ## Acceptance Criteria
371
+
372
+ - [ ] Task completed successfully
373
+ `;
374
+
375
+ // Ensure directory exists
376
+ if (!fs.existsSync(tasksDir)) {
377
+ fs.mkdirSync(tasksDir, { recursive: true });
378
+ }
379
+
380
+ // Write file
381
+ fs.writeFileSync(filePath, content, 'utf8');
382
+
383
+ console.log(`[Tasks] Created: ${filename}`);
384
+
385
+ // Invalidate list cache — the pending directory mtime will have changed,
386
+ // but proactively clearing is cheaper than letting the next request do
387
+ // a stat comparison that would miss in-process writes on some OS/FS combos.
388
+ invalidateTaskCache();
389
+
390
+ // Return created task
391
+ return readTaskFile(filePath);
392
+ }
393
+
394
+ /**
395
+ * Generate a unique task ID
396
+ * @param {string} prefix - ID prefix (e.g., 'TASK', 'AUTO')
397
+ * @returns {string} Generated ID
398
+ */
399
+ function generateTaskId(prefix = 'TASK') {
400
+ const timestamp = Date.now().toString(36).toUpperCase();
401
+ const random = Math.random().toString(36).substring(2, 5).toUpperCase();
402
+ return `${prefix}-${timestamp}-${random}`;
403
+ }
404
+
405
+ module.exports = {
406
+ listTasks,
407
+ getTask,
408
+ createTask,
409
+ readTaskFile,
410
+ readTaskFull,
411
+ parseFrontmatter,
412
+ invalidateTaskCache,
413
+ // Exported for testing only:
414
+ isTaskCacheValid,
415
+ recordTaskCacheMtimes
416
+ };
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" data-theme="dark">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>&#x1F525;</text></svg>">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Vibe Forge Dashboard</title>
8
+ </head>
9
+ <body>
10
+ <div id="app"></div>
11
+ <script type="module" src="/src/main.js"></script>
12
+ </body>
13
+ </html>
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "vibe-forge-dashboard",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "devDependencies": {
12
+ "@sveltejs/vite-plugin-svelte": "^4.0.0",
13
+ "svelte": "^5.0.0",
14
+ "vite": "^5.4.0"
15
+ }
16
+ }
@@ -0,0 +1,222 @@
1
+ <script>
2
+ import { onMount, onDestroy } from 'svelte';
3
+ import { initTheme, toggleTheme } from './lib/stores/theme.js';
4
+ import { connectWebSocket, disconnectWebSocket, startPeriodicRefresh, stopPeriodicRefresh, refreshTasks, refreshAgents, refreshIssues, refreshAll } from './lib/stores/websocket.js';
5
+ import { pendingDispatch, closeDispatchModal, issues, openDispatchModal } from './lib/stores/issues.js';
6
+ import { clearSelectedTask, filteredTasks, currentTaskFilter, toggleTaskExpand, setTaskFilter } from './lib/stores/tasks.js';
7
+ import { focusedPanel, focusedItemIndex, setFocusedPanel, mobileActivePanel, setMobilePanel, showKeyboardHelp, openKeyboardHelp, closeKeyboardHelp } from './lib/stores/ui.js';
8
+ import { get } from 'svelte/store';
9
+
10
+ import Header from './lib/components/Header.svelte';
11
+ import Footer from './lib/components/Footer.svelte';
12
+ import TasksPanel from './lib/components/TasksPanel.svelte';
13
+ import AgentsPanel from './lib/components/AgentsPanel.svelte';
14
+ import NotificationsPanel from './lib/components/NotificationsPanel.svelte';
15
+ import IssuesPanel from './lib/components/IssuesPanel.svelte';
16
+ import DispatchModal from './lib/components/DispatchModal.svelte';
17
+ import KeyboardShortcutsModal from './lib/components/KeyboardShortcutsModal.svelte';
18
+ import Toast from './lib/components/Toast.svelte';
19
+ import MobileTabs from './lib/components/MobileTabs.svelte';
20
+
21
+ onMount(() => {
22
+ initTheme();
23
+ connectWebSocket();
24
+ startPeriodicRefresh();
25
+
26
+ // Setup keyboard shortcuts
27
+ document.addEventListener('keydown', handleKeydown);
28
+ });
29
+
30
+ onDestroy(() => {
31
+ disconnectWebSocket();
32
+ document.removeEventListener('keydown', handleKeydown);
33
+ });
34
+
35
+ function handleKeydown(event) {
36
+ // Don't handle shortcuts if typing in input
37
+ if (event.target.matches('input, textarea, select')) return;
38
+
39
+ // Check if modal is open
40
+ const dispatchOpen = get(pendingDispatch) !== null;
41
+ const keyboardHelpOpen = get(showKeyboardHelp);
42
+ const modalOpen = dispatchOpen || keyboardHelpOpen;
43
+
44
+ if (event.key === 'Escape') {
45
+ if (dispatchOpen) {
46
+ closeDispatchModal();
47
+ return;
48
+ }
49
+ if (keyboardHelpOpen) {
50
+ closeKeyboardHelp();
51
+ return;
52
+ }
53
+ // Deselect current item
54
+ clearSelectedTask();
55
+ return;
56
+ }
57
+
58
+ if (modalOpen) return;
59
+
60
+ switch (event.key) {
61
+ case '1':
62
+ focusPanelByName('tasks');
63
+ break;
64
+ case '2':
65
+ focusPanelByName('agents');
66
+ break;
67
+ case '3':
68
+ focusPanelByName('notifications');
69
+ break;
70
+ case '4':
71
+ focusPanelByName('issues');
72
+ break;
73
+ case 'j':
74
+ case 'ArrowDown':
75
+ navigateList('down');
76
+ event.preventDefault();
77
+ break;
78
+ case 'k':
79
+ case 'ArrowUp':
80
+ navigateList('up');
81
+ event.preventDefault();
82
+ break;
83
+ case 'Enter':
84
+ activateSelectedItem();
85
+ break;
86
+ case 'd':
87
+ dispatchSelectedIssue();
88
+ break;
89
+ case 'r':
90
+ refreshCurrentPanel();
91
+ break;
92
+ case 't':
93
+ toggleTheme();
94
+ break;
95
+ case '?':
96
+ openKeyboardHelp();
97
+ event.preventDefault();
98
+ break;
99
+ }
100
+ }
101
+
102
+ function focusPanelByName(panelName) {
103
+ setFocusedPanel(panelName);
104
+ focusedItemIndex.set(-1);
105
+
106
+ const panel = document.getElementById(`${panelName}-panel`);
107
+ panel?.focus();
108
+
109
+ // On mobile, also switch the active panel
110
+ if (window.innerWidth < 768) {
111
+ setMobilePanel(panelName);
112
+ }
113
+ }
114
+
115
+ function navigateList(direction) {
116
+ const currentPanel = get(focusedPanel);
117
+ if (!currentPanel) {
118
+ focusPanelByName('tasks');
119
+ return;
120
+ }
121
+
122
+ const panel = document.getElementById(`${currentPanel}-panel`);
123
+ const items = panel?.querySelectorAll('.card');
124
+ if (!items || items.length === 0) return;
125
+
126
+ if (direction === 'down') {
127
+ currentFocusedItemIndex = Math.min(currentFocusedItemIndex + 1, items.length - 1);
128
+ } else {
129
+ currentFocusedItemIndex = Math.max(currentFocusedItemIndex - 1, 0);
130
+ }
131
+
132
+ focusedItemIndex.set(currentFocusedItemIndex);
133
+
134
+ items.forEach((item, i) => {
135
+ item.classList.toggle('selected', i === currentFocusedItemIndex);
136
+ });
137
+
138
+ items[currentFocusedItemIndex]?.scrollIntoView({ block: 'nearest' });
139
+ items[currentFocusedItemIndex]?.focus();
140
+ }
141
+
142
+ function activateSelectedItem() {
143
+ const currentPanel = get(focusedPanel);
144
+
145
+ if (currentPanel === 'tasks' && currentFocusedItemIndex >= 0) {
146
+ const taskCards = document.querySelectorAll('#tasks-list .task-card');
147
+ const card = taskCards[currentFocusedItemIndex];
148
+ if (card) {
149
+ const taskId = card.dataset.taskId;
150
+ toggleTaskExpand(taskId);
151
+ }
152
+ } else if (currentPanel === 'issues' && currentFocusedItemIndex >= 0) {
153
+ dispatchSelectedIssue();
154
+ }
155
+ }
156
+
157
+ function dispatchSelectedIssue() {
158
+ const currentPanel = get(focusedPanel);
159
+ if (currentPanel !== 'issues') return;
160
+
161
+ const issueCards = document.querySelectorAll('#issues-list .issue-card');
162
+ const card = issueCards[currentFocusedItemIndex];
163
+ if (card) {
164
+ const issueId = card.dataset.issueId;
165
+ const issuesList = get(issues);
166
+ const issue = issuesList.find(i => i.id === issueId);
167
+ if (issue) {
168
+ openDispatchModal(issue);
169
+ }
170
+ }
171
+ }
172
+
173
+ function refreshCurrentPanel() {
174
+ const currentPanel = get(focusedPanel);
175
+ switch (currentPanel) {
176
+ case 'tasks':
177
+ refreshTasks();
178
+ break;
179
+ case 'agents':
180
+ refreshAgents();
181
+ break;
182
+ case 'issues':
183
+ refreshIssues();
184
+ break;
185
+ default:
186
+ refreshAll();
187
+ }
188
+ }
189
+ </script>
190
+
191
+ <!-- Skip Links for Accessibility -->
192
+ <a href="#main-content" class="skip-link">Skip to main content</a>
193
+ <a href="#tasks-panel" class="skip-link">Skip to tasks</a>
194
+ <a href="#issues-panel" class="skip-link">Skip to issues</a>
195
+
196
+ <Header />
197
+
198
+ <!-- Main Content -->
199
+ <!-- svelte-ignore a11y_no_redundant_roles -->
200
+ <main id="main-content" class="main" role="main">
201
+ <!-- Top Row: Tasks, Agents, Notifications -->
202
+ <div class="panels-top">
203
+ <TasksPanel />
204
+ <AgentsPanel />
205
+ <NotificationsPanel />
206
+ </div>
207
+
208
+ <!-- Bottom Row: Issues Panel -->
209
+ <IssuesPanel />
210
+ </main>
211
+
212
+ <Footer />
213
+
214
+ <!-- Modals -->
215
+ <DispatchModal />
216
+ <KeyboardShortcutsModal />
217
+
218
+ <!-- Toast Container -->
219
+ <Toast />
220
+
221
+ <!-- Mobile Tab Bar -->
222
+ <MobileTabs />