specsmd 0.0.0-dev.86 → 0.0.0-dev.87
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/README.md +15 -0
- package/bin/cli.js +15 -1
- package/flows/fire/agents/builder/agent.md +2 -2
- package/flows/fire/agents/builder/skills/code-review/SKILL.md +1 -1
- package/flows/fire/agents/builder/skills/run-execute/SKILL.md +16 -7
- package/flows/fire/agents/builder/skills/run-execute/scripts/complete-run.cjs +22 -3
- package/flows/fire/agents/builder/skills/run-execute/scripts/init-run.cjs +63 -20
- package/flows/fire/agents/builder/skills/run-execute/scripts/update-checkpoint.cjs +254 -0
- package/flows/fire/agents/builder/skills/run-execute/scripts/update-phase.cjs +17 -6
- package/flows/fire/agents/builder/skills/run-status/SKILL.md +1 -1
- package/flows/fire/agents/orchestrator/agent.md +1 -1
- package/flows/fire/agents/orchestrator/skills/status/SKILL.md +2 -2
- package/flows/fire/memory-bank.yaml +4 -4
- package/lib/dashboard/aidlc/parser.js +581 -0
- package/lib/dashboard/fire/model.js +382 -0
- package/lib/dashboard/fire/parser.js +470 -0
- package/lib/dashboard/flow-detect.js +86 -0
- package/lib/dashboard/git/changes.js +362 -0
- package/lib/dashboard/git/worktrees.js +248 -0
- package/lib/dashboard/index.js +709 -0
- package/lib/dashboard/runtime/watch-runtime.js +122 -0
- package/lib/dashboard/simple/parser.js +293 -0
- package/lib/dashboard/tui/app.js +1675 -0
- package/lib/dashboard/tui/components/error-banner.js +35 -0
- package/lib/dashboard/tui/components/header.js +60 -0
- package/lib/dashboard/tui/components/help-footer.js +15 -0
- package/lib/dashboard/tui/components/stats-strip.js +35 -0
- package/lib/dashboard/tui/file-entries.js +383 -0
- package/lib/dashboard/tui/flow-builders.js +991 -0
- package/lib/dashboard/tui/git-builders.js +218 -0
- package/lib/dashboard/tui/helpers.js +236 -0
- package/lib/dashboard/tui/overlays.js +242 -0
- package/lib/dashboard/tui/preview.js +220 -0
- package/lib/dashboard/tui/renderer.js +76 -0
- package/lib/dashboard/tui/row-builders.js +797 -0
- package/lib/dashboard/tui/sections.js +45 -0
- package/lib/dashboard/tui/store.js +44 -0
- package/lib/dashboard/tui/views/overview-view.js +61 -0
- package/lib/dashboard/tui/views/runs-view.js +93 -0
- package/lib/dashboard/tui/worktree-builders.js +229 -0
- package/lib/installers/CodexInstaller.js +72 -1
- package/package.json +7 -3
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
const chokidar = require('chokidar');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
function createDebouncedTrigger(callback, delayMs) {
|
|
5
|
+
let timeoutId = null;
|
|
6
|
+
|
|
7
|
+
const trigger = () => {
|
|
8
|
+
if (timeoutId != null) {
|
|
9
|
+
clearTimeout(timeoutId);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
timeoutId = setTimeout(() => {
|
|
13
|
+
timeoutId = null;
|
|
14
|
+
callback();
|
|
15
|
+
}, delayMs);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const cancel = () => {
|
|
19
|
+
if (timeoutId != null) {
|
|
20
|
+
clearTimeout(timeoutId);
|
|
21
|
+
timeoutId = null;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
trigger,
|
|
27
|
+
cancel,
|
|
28
|
+
isPending: () => timeoutId != null
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function createWatchRuntime(options) {
|
|
33
|
+
const {
|
|
34
|
+
rootPath,
|
|
35
|
+
rootPaths,
|
|
36
|
+
onRefresh,
|
|
37
|
+
onError,
|
|
38
|
+
debounceMs = 250
|
|
39
|
+
} = options;
|
|
40
|
+
|
|
41
|
+
const roots = Array.from(new Set([
|
|
42
|
+
...(Array.isArray(rootPaths) ? rootPaths : []),
|
|
43
|
+
...(typeof rootPath === 'string' ? [rootPath] : [])
|
|
44
|
+
].filter((value) => typeof value === 'string' && value.trim() !== '')));
|
|
45
|
+
|
|
46
|
+
if (roots.length === 0) {
|
|
47
|
+
throw new Error('rootPath or rootPaths is required for watch runtime');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (typeof onRefresh !== 'function') {
|
|
51
|
+
throw new Error('onRefresh callback is required for watch runtime');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const reportError = typeof onError === 'function' ? onError : () => {};
|
|
55
|
+
|
|
56
|
+
let watcher = null;
|
|
57
|
+
let started = false;
|
|
58
|
+
const debounced = createDebouncedTrigger(onRefresh, debounceMs);
|
|
59
|
+
|
|
60
|
+
const watchTargets = roots.flatMap((baseRoot) => ([
|
|
61
|
+
path.join(baseRoot, 'state.yaml'),
|
|
62
|
+
path.join(baseRoot, 'intents'),
|
|
63
|
+
path.join(baseRoot, 'runs'),
|
|
64
|
+
path.join(baseRoot, 'standards'),
|
|
65
|
+
path.join(baseRoot, 'bolts'),
|
|
66
|
+
path.join(baseRoot, 'specs')
|
|
67
|
+
]));
|
|
68
|
+
|
|
69
|
+
function start() {
|
|
70
|
+
if (started) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
started = true;
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
watcher = chokidar.watch(watchTargets, {
|
|
78
|
+
persistent: true,
|
|
79
|
+
ignoreInitial: true,
|
|
80
|
+
awaitWriteFinish: {
|
|
81
|
+
stabilityThreshold: 150,
|
|
82
|
+
pollInterval: 50
|
|
83
|
+
},
|
|
84
|
+
depth: 10
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
watcher.on('all', () => {
|
|
88
|
+
debounced.trigger();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
watcher.on('error', (error) => {
|
|
92
|
+
reportError(error);
|
|
93
|
+
});
|
|
94
|
+
} catch (error) {
|
|
95
|
+
reportError(error);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function close() {
|
|
100
|
+
debounced.cancel();
|
|
101
|
+
|
|
102
|
+
if (watcher) {
|
|
103
|
+
const activeWatcher = watcher;
|
|
104
|
+
watcher = null;
|
|
105
|
+
started = false;
|
|
106
|
+
await activeWatcher.close();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
start,
|
|
112
|
+
close,
|
|
113
|
+
isActive: () => started,
|
|
114
|
+
hasPendingRefresh: () => debounced.isPending(),
|
|
115
|
+
getRoots: () => [...roots]
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = {
|
|
120
|
+
createDebouncedTrigger,
|
|
121
|
+
createWatchRuntime
|
|
122
|
+
};
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
function readFileSafe(filePath) {
|
|
5
|
+
try {
|
|
6
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
7
|
+
} catch {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function listSubdirectories(dirPath) {
|
|
13
|
+
try {
|
|
14
|
+
return fs.readdirSync(dirPath, { withFileTypes: true })
|
|
15
|
+
.filter((entry) => entry.isDirectory())
|
|
16
|
+
.map((entry) => entry.name)
|
|
17
|
+
.sort((a, b) => a.localeCompare(b));
|
|
18
|
+
} catch {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function parseTaskChecklist(content) {
|
|
24
|
+
const lines = String(content || '').split(/\r?\n/);
|
|
25
|
+
const tasks = [];
|
|
26
|
+
|
|
27
|
+
lines.forEach((line, index) => {
|
|
28
|
+
const match = line.match(/^\s*[-*]\s*\[( |x|X)\](\*)?\s+(.+)$/);
|
|
29
|
+
if (!match) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
tasks.push({
|
|
34
|
+
line: index + 1,
|
|
35
|
+
done: match[1].toLowerCase() === 'x',
|
|
36
|
+
optional: match[2] === '*',
|
|
37
|
+
text: match[3].trim()
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return tasks;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getLatestTimestamp(paths) {
|
|
45
|
+
let latest = 0;
|
|
46
|
+
|
|
47
|
+
for (const filePath of paths) {
|
|
48
|
+
try {
|
|
49
|
+
const stats = fs.statSync(filePath);
|
|
50
|
+
if (stats.mtimeMs > latest) {
|
|
51
|
+
latest = stats.mtimeMs;
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
// Ignore missing files.
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return latest > 0 ? new Date(latest).toISOString() : undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function deriveSpecState(hasRequirements, hasDesign, hasTasks, taskStats) {
|
|
62
|
+
if (!hasRequirements) {
|
|
63
|
+
return 'requirements_pending';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!hasDesign) {
|
|
67
|
+
return 'design_pending';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!hasTasks) {
|
|
71
|
+
return 'tasks_pending';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (taskStats.total === 0) {
|
|
75
|
+
return 'tasks_pending';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (taskStats.completed >= taskStats.total) {
|
|
79
|
+
return 'completed';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (taskStats.completed > 0) {
|
|
83
|
+
return 'in_progress';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return 'ready';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function statePriority(state) {
|
|
90
|
+
switch (state) {
|
|
91
|
+
case 'in_progress':
|
|
92
|
+
return 0;
|
|
93
|
+
case 'ready':
|
|
94
|
+
return 1;
|
|
95
|
+
case 'tasks_pending':
|
|
96
|
+
return 2;
|
|
97
|
+
case 'design_pending':
|
|
98
|
+
return 3;
|
|
99
|
+
case 'requirements_pending':
|
|
100
|
+
return 4;
|
|
101
|
+
case 'completed':
|
|
102
|
+
return 5;
|
|
103
|
+
default:
|
|
104
|
+
return 6;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function phaseForState(state) {
|
|
109
|
+
if (state === 'requirements_pending') {
|
|
110
|
+
return 'requirements';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (state === 'design_pending') {
|
|
114
|
+
return 'design';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return 'tasks';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function parseSpec(specsPath, specName, warnings) {
|
|
121
|
+
const specPath = path.join(specsPath, specName);
|
|
122
|
+
|
|
123
|
+
const requirementsPath = path.join(specPath, 'requirements.md');
|
|
124
|
+
const designPath = path.join(specPath, 'design.md');
|
|
125
|
+
const tasksPath = path.join(specPath, 'tasks.md');
|
|
126
|
+
|
|
127
|
+
const hasRequirements = fs.existsSync(requirementsPath);
|
|
128
|
+
const hasDesign = fs.existsSync(designPath);
|
|
129
|
+
const hasTasks = fs.existsSync(tasksPath);
|
|
130
|
+
|
|
131
|
+
if (!hasRequirements) {
|
|
132
|
+
warnings.push(`Spec ${specName} is missing requirements.md.`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const tasksContent = hasTasks ? readFileSafe(tasksPath) : null;
|
|
136
|
+
const tasks = tasksContent ? parseTaskChecklist(tasksContent) : [];
|
|
137
|
+
|
|
138
|
+
const taskStats = {
|
|
139
|
+
total: tasks.length,
|
|
140
|
+
completed: tasks.filter((task) => task.done).length,
|
|
141
|
+
optional: tasks.filter((task) => task.optional).length
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const state = deriveSpecState(hasRequirements, hasDesign, hasTasks, taskStats);
|
|
145
|
+
const updatedAt = getLatestTimestamp([requirementsPath, designPath, tasksPath]);
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
id: specName,
|
|
149
|
+
name: specName,
|
|
150
|
+
path: specPath,
|
|
151
|
+
state,
|
|
152
|
+
phase: phaseForState(state),
|
|
153
|
+
hasRequirements,
|
|
154
|
+
hasDesign,
|
|
155
|
+
hasTasks,
|
|
156
|
+
tasks,
|
|
157
|
+
tasksTotal: taskStats.total,
|
|
158
|
+
tasksCompleted: taskStats.completed,
|
|
159
|
+
tasksPending: Math.max(taskStats.total - taskStats.completed, 0),
|
|
160
|
+
optionalTasks: taskStats.optional,
|
|
161
|
+
updatedAt
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function buildProjectMetadata(workspacePath) {
|
|
166
|
+
const packageJsonPath = path.join(workspacePath, 'package.json');
|
|
167
|
+
const fallbackName = path.basename(workspacePath);
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
const parsed = JSON.parse(readFileSafe(packageJsonPath) || '{}');
|
|
171
|
+
return {
|
|
172
|
+
name: typeof parsed.name === 'string' ? parsed.name : fallbackName,
|
|
173
|
+
description: typeof parsed.description === 'string' ? parsed.description : undefined
|
|
174
|
+
};
|
|
175
|
+
} catch {
|
|
176
|
+
return { name: fallbackName };
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function buildStats(specs) {
|
|
181
|
+
const totalSpecs = specs.length;
|
|
182
|
+
const completedSpecs = specs.filter((spec) => spec.state === 'completed').length;
|
|
183
|
+
const inProgressSpecs = specs.filter((spec) => spec.state === 'in_progress').length;
|
|
184
|
+
const readySpecs = specs.filter((spec) => spec.state === 'ready').length;
|
|
185
|
+
const designPendingSpecs = specs.filter((spec) => spec.state === 'design_pending').length;
|
|
186
|
+
const tasksPendingSpecs = specs.filter((spec) => spec.state === 'tasks_pending').length;
|
|
187
|
+
const requirementsPendingSpecs = specs.filter((spec) => spec.state === 'requirements_pending').length;
|
|
188
|
+
const pendingSpecs = totalSpecs - completedSpecs;
|
|
189
|
+
|
|
190
|
+
const totalTasks = specs.reduce((sum, spec) => sum + spec.tasksTotal, 0);
|
|
191
|
+
const completedTasks = specs.reduce((sum, spec) => sum + spec.tasksCompleted, 0);
|
|
192
|
+
const optionalTasks = specs.reduce((sum, spec) => sum + spec.optionalTasks, 0);
|
|
193
|
+
const pendingTasks = Math.max(totalTasks - completedTasks, 0);
|
|
194
|
+
|
|
195
|
+
const progressPercent = totalTasks > 0
|
|
196
|
+
? Math.round((completedTasks / totalTasks) * 100)
|
|
197
|
+
: 0;
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
totalSpecs,
|
|
201
|
+
completedSpecs,
|
|
202
|
+
inProgressSpecs,
|
|
203
|
+
readySpecs,
|
|
204
|
+
pendingSpecs,
|
|
205
|
+
designPendingSpecs,
|
|
206
|
+
tasksPendingSpecs,
|
|
207
|
+
requirementsPendingSpecs,
|
|
208
|
+
totalTasks,
|
|
209
|
+
completedTasks,
|
|
210
|
+
pendingTasks,
|
|
211
|
+
optionalTasks,
|
|
212
|
+
activeSpecsCount: inProgressSpecs + readySpecs,
|
|
213
|
+
progressPercent
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function parseSimpleDashboard(workspacePath) {
|
|
218
|
+
const rootPath = path.join(workspacePath, 'specs');
|
|
219
|
+
|
|
220
|
+
if (!fs.existsSync(rootPath) || !fs.statSync(rootPath).isDirectory()) {
|
|
221
|
+
return {
|
|
222
|
+
ok: false,
|
|
223
|
+
error: {
|
|
224
|
+
code: 'SIMPLE_NOT_FOUND',
|
|
225
|
+
message: `No Simple flow workspace found at ${rootPath}`,
|
|
226
|
+
hint: 'Run this command from a workspace containing specs/ or choose --flow fire/aidlc.'
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const warnings = [];
|
|
232
|
+
const specFolders = listSubdirectories(rootPath);
|
|
233
|
+
const specs = specFolders
|
|
234
|
+
.map((specFolder) => parseSpec(rootPath, specFolder, warnings))
|
|
235
|
+
.sort((a, b) => {
|
|
236
|
+
const priorityDiff = statePriority(a.state) - statePriority(b.state);
|
|
237
|
+
if (priorityDiff !== 0) {
|
|
238
|
+
return priorityDiff;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const aTime = a.updatedAt ? Date.parse(a.updatedAt) : 0;
|
|
242
|
+
const bTime = b.updatedAt ? Date.parse(b.updatedAt) : 0;
|
|
243
|
+
if (bTime !== aTime) {
|
|
244
|
+
return bTime - aTime;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return a.name.localeCompare(b.name);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
if (specs.length === 0) {
|
|
251
|
+
warnings.push('No specs found under specs/.');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const activeSpecs = specs.filter((spec) => spec.state !== 'completed');
|
|
255
|
+
const completedSpecs = specs
|
|
256
|
+
.filter((spec) => spec.state === 'completed')
|
|
257
|
+
.sort((a, b) => {
|
|
258
|
+
const aTime = a.updatedAt ? Date.parse(a.updatedAt) : 0;
|
|
259
|
+
const bTime = b.updatedAt ? Date.parse(b.updatedAt) : 0;
|
|
260
|
+
if (bTime !== aTime) {
|
|
261
|
+
return bTime - aTime;
|
|
262
|
+
}
|
|
263
|
+
return b.name.localeCompare(a.name);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const stats = buildStats(specs);
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
ok: true,
|
|
270
|
+
snapshot: {
|
|
271
|
+
flow: 'simple',
|
|
272
|
+
isProject: true,
|
|
273
|
+
initialized: true,
|
|
274
|
+
workspacePath,
|
|
275
|
+
rootPath,
|
|
276
|
+
version: '1.0.0',
|
|
277
|
+
project: buildProjectMetadata(workspacePath),
|
|
278
|
+
specs,
|
|
279
|
+
activeSpecs,
|
|
280
|
+
completedSpecs,
|
|
281
|
+
pendingSpecs: activeSpecs,
|
|
282
|
+
standards: [],
|
|
283
|
+
stats,
|
|
284
|
+
warnings,
|
|
285
|
+
generatedAt: new Date().toISOString()
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
module.exports = {
|
|
291
|
+
parseTaskChecklist,
|
|
292
|
+
parseSimpleDashboard
|
|
293
|
+
};
|