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.
Files changed (42) hide show
  1. package/README.md +15 -0
  2. package/bin/cli.js +15 -1
  3. package/flows/fire/agents/builder/agent.md +2 -2
  4. package/flows/fire/agents/builder/skills/code-review/SKILL.md +1 -1
  5. package/flows/fire/agents/builder/skills/run-execute/SKILL.md +16 -7
  6. package/flows/fire/agents/builder/skills/run-execute/scripts/complete-run.cjs +22 -3
  7. package/flows/fire/agents/builder/skills/run-execute/scripts/init-run.cjs +63 -20
  8. package/flows/fire/agents/builder/skills/run-execute/scripts/update-checkpoint.cjs +254 -0
  9. package/flows/fire/agents/builder/skills/run-execute/scripts/update-phase.cjs +17 -6
  10. package/flows/fire/agents/builder/skills/run-status/SKILL.md +1 -1
  11. package/flows/fire/agents/orchestrator/agent.md +1 -1
  12. package/flows/fire/agents/orchestrator/skills/status/SKILL.md +2 -2
  13. package/flows/fire/memory-bank.yaml +4 -4
  14. package/lib/dashboard/aidlc/parser.js +581 -0
  15. package/lib/dashboard/fire/model.js +382 -0
  16. package/lib/dashboard/fire/parser.js +470 -0
  17. package/lib/dashboard/flow-detect.js +86 -0
  18. package/lib/dashboard/git/changes.js +362 -0
  19. package/lib/dashboard/git/worktrees.js +248 -0
  20. package/lib/dashboard/index.js +709 -0
  21. package/lib/dashboard/runtime/watch-runtime.js +122 -0
  22. package/lib/dashboard/simple/parser.js +293 -0
  23. package/lib/dashboard/tui/app.js +1675 -0
  24. package/lib/dashboard/tui/components/error-banner.js +35 -0
  25. package/lib/dashboard/tui/components/header.js +60 -0
  26. package/lib/dashboard/tui/components/help-footer.js +15 -0
  27. package/lib/dashboard/tui/components/stats-strip.js +35 -0
  28. package/lib/dashboard/tui/file-entries.js +383 -0
  29. package/lib/dashboard/tui/flow-builders.js +991 -0
  30. package/lib/dashboard/tui/git-builders.js +218 -0
  31. package/lib/dashboard/tui/helpers.js +236 -0
  32. package/lib/dashboard/tui/overlays.js +242 -0
  33. package/lib/dashboard/tui/preview.js +220 -0
  34. package/lib/dashboard/tui/renderer.js +76 -0
  35. package/lib/dashboard/tui/row-builders.js +797 -0
  36. package/lib/dashboard/tui/sections.js +45 -0
  37. package/lib/dashboard/tui/store.js +44 -0
  38. package/lib/dashboard/tui/views/overview-view.js +61 -0
  39. package/lib/dashboard/tui/views/runs-view.js +93 -0
  40. package/lib/dashboard/tui/worktree-builders.js +229 -0
  41. package/lib/installers/CodexInstaller.js +72 -1
  42. 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
+ };