@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,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'>🔥</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 />
|