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,470 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const yaml = require('js-yaml');
|
|
4
|
+
|
|
5
|
+
const {
|
|
6
|
+
normalizeStatus,
|
|
7
|
+
normalizeMode,
|
|
8
|
+
normalizeScope,
|
|
9
|
+
normalizeComplexity,
|
|
10
|
+
normalizeState,
|
|
11
|
+
deriveIntentStatus,
|
|
12
|
+
calculateStats,
|
|
13
|
+
parseDependencies,
|
|
14
|
+
buildPendingItems,
|
|
15
|
+
normalizeRunWorkItem
|
|
16
|
+
} = require('./model');
|
|
17
|
+
|
|
18
|
+
const STANDARD_TYPES = [
|
|
19
|
+
'constitution',
|
|
20
|
+
'tech-stack',
|
|
21
|
+
'coding-standards',
|
|
22
|
+
'testing-standards',
|
|
23
|
+
'system-architecture'
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
function parseFrontmatter(content) {
|
|
27
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
28
|
+
if (!match) {
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
return yaml.load(match[1]) || {};
|
|
34
|
+
} catch {
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readFileSafe(filePath) {
|
|
40
|
+
try {
|
|
41
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function listSubdirectories(dirPath) {
|
|
48
|
+
try {
|
|
49
|
+
return fs.readdirSync(dirPath, { withFileTypes: true })
|
|
50
|
+
.filter((entry) => entry.isDirectory())
|
|
51
|
+
.map((entry) => entry.name)
|
|
52
|
+
.sort((a, b) => a.localeCompare(b));
|
|
53
|
+
} catch {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function listMarkdownFiles(dirPath) {
|
|
59
|
+
try {
|
|
60
|
+
return fs.readdirSync(dirPath)
|
|
61
|
+
.filter((file) => file.endsWith('.md'))
|
|
62
|
+
.sort((a, b) => a.localeCompare(b));
|
|
63
|
+
} catch {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getFirstStringValue(record, keys) {
|
|
69
|
+
if (!record || typeof record !== 'object') {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (const key of keys) {
|
|
74
|
+
const value = record[key];
|
|
75
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function parseRunLog(runLogPath) {
|
|
84
|
+
const content = readFileSafe(runLogPath);
|
|
85
|
+
if (!content) {
|
|
86
|
+
return {
|
|
87
|
+
scope: undefined,
|
|
88
|
+
workItems: [],
|
|
89
|
+
currentItem: null,
|
|
90
|
+
startedAt: undefined,
|
|
91
|
+
completedAt: undefined,
|
|
92
|
+
checkpointState: undefined,
|
|
93
|
+
currentCheckpoint: undefined
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const frontmatter = parseFrontmatter(content);
|
|
98
|
+
const currentItem = getFirstStringValue(frontmatter, ['current_item', 'currentItem', 'work_item', 'workItem']);
|
|
99
|
+
const itemMode = getFirstStringValue(frontmatter, ['mode']);
|
|
100
|
+
const itemStatus = getFirstStringValue(frontmatter, ['status']);
|
|
101
|
+
const itemPhase = getFirstStringValue(frontmatter, ['current_phase', 'currentPhase']);
|
|
102
|
+
const itemCheckpointState = getFirstStringValue(frontmatter, [
|
|
103
|
+
'checkpoint_state',
|
|
104
|
+
'checkpointState',
|
|
105
|
+
'approval_state',
|
|
106
|
+
'approvalState'
|
|
107
|
+
]);
|
|
108
|
+
const itemCheckpoint = getFirstStringValue(frontmatter, ['current_checkpoint', 'currentCheckpoint', 'checkpoint']);
|
|
109
|
+
|
|
110
|
+
const workItemsRaw = Array.isArray(frontmatter.work_items)
|
|
111
|
+
? frontmatter.work_items
|
|
112
|
+
: (Array.isArray(frontmatter.workItems) ? frontmatter.workItems : []);
|
|
113
|
+
|
|
114
|
+
if (workItemsRaw.length === 0 && typeof currentItem === 'string' && currentItem !== '') {
|
|
115
|
+
workItemsRaw.push({
|
|
116
|
+
id: currentItem,
|
|
117
|
+
mode: itemMode,
|
|
118
|
+
status: itemStatus,
|
|
119
|
+
current_phase: itemPhase,
|
|
120
|
+
checkpoint_state: itemCheckpointState,
|
|
121
|
+
current_checkpoint: itemCheckpoint
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const workItems = workItemsRaw
|
|
126
|
+
.map((item) => normalizeRunWorkItem(item))
|
|
127
|
+
.filter((item) => item.id !== '');
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
scope: normalizeScope(frontmatter.scope),
|
|
131
|
+
workItems,
|
|
132
|
+
currentItem: currentItem || null,
|
|
133
|
+
startedAt: typeof frontmatter.started === 'string' ? frontmatter.started : undefined,
|
|
134
|
+
completedAt: typeof frontmatter.completed === 'string'
|
|
135
|
+
? frontmatter.completed
|
|
136
|
+
: undefined,
|
|
137
|
+
checkpointState: itemCheckpointState,
|
|
138
|
+
currentCheckpoint: itemCheckpoint
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function mergeRunWorkItems(primaryItems, fallbackItems) {
|
|
143
|
+
const primary = Array.isArray(primaryItems) ? primaryItems : [];
|
|
144
|
+
const fallback = Array.isArray(fallbackItems) ? fallbackItems : [];
|
|
145
|
+
|
|
146
|
+
if (primary.length === 0) {
|
|
147
|
+
return fallback;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (fallback.length === 0) {
|
|
151
|
+
return primary;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const fallbackById = new Map(fallback.map((item) => [item.id, item]));
|
|
155
|
+
const merged = primary.map((item) => {
|
|
156
|
+
const fallbackItem = fallbackById.get(item.id);
|
|
157
|
+
if (!fallbackItem) {
|
|
158
|
+
return item;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
...fallbackItem,
|
|
163
|
+
...item,
|
|
164
|
+
checkpointState: item.checkpointState || fallbackItem.checkpointState,
|
|
165
|
+
currentCheckpoint: item.currentCheckpoint || fallbackItem.currentCheckpoint,
|
|
166
|
+
currentPhase: item.currentPhase || fallbackItem.currentPhase
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const knownIds = new Set(merged.map((item) => item.id));
|
|
171
|
+
for (const fallbackItem of fallback) {
|
|
172
|
+
if (!knownIds.has(fallbackItem.id)) {
|
|
173
|
+
merged.push(fallbackItem);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return merged;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function scanWorkItems(intentPath, intentId, stateWorkItems, warnings) {
|
|
181
|
+
const workItemsPath = path.join(intentPath, 'work-items');
|
|
182
|
+
const fileWorkItemIds = listMarkdownFiles(workItemsPath)
|
|
183
|
+
.map((file) => file.replace(/\.md$/, ''));
|
|
184
|
+
|
|
185
|
+
const stateWorkItemIds = (stateWorkItems || []).map((item) => item.id).filter(Boolean);
|
|
186
|
+
const uniqueIds = Array.from(new Set([...fileWorkItemIds, ...stateWorkItemIds])).sort((a, b) => a.localeCompare(b));
|
|
187
|
+
|
|
188
|
+
const stateMap = new Map((stateWorkItems || []).map((item) => [item.id, item]));
|
|
189
|
+
|
|
190
|
+
return uniqueIds.map((workItemId) => {
|
|
191
|
+
const stateItem = stateMap.get(workItemId);
|
|
192
|
+
const filePath = path.join(workItemsPath, `${workItemId}.md`);
|
|
193
|
+
|
|
194
|
+
let frontmatter = {};
|
|
195
|
+
if (fs.existsSync(filePath)) {
|
|
196
|
+
frontmatter = parseFrontmatter(readFileSafe(filePath) || '');
|
|
197
|
+
} else if (stateItem) {
|
|
198
|
+
warnings.push(`Work item ${intentId}/${workItemId} exists in state.yaml but markdown file is missing.`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const dependencies = parseDependencies(frontmatter.depends_on ?? frontmatter.dependencies);
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
id: workItemId,
|
|
205
|
+
intentId,
|
|
206
|
+
title: typeof frontmatter.title === 'string' ? frontmatter.title : workItemId,
|
|
207
|
+
status: normalizeStatus(stateItem?.status || frontmatter.status) || 'pending',
|
|
208
|
+
mode: normalizeMode(stateItem?.mode || frontmatter.mode) || 'confirm',
|
|
209
|
+
complexity: normalizeComplexity(frontmatter.complexity) || 'medium',
|
|
210
|
+
filePath,
|
|
211
|
+
description: typeof frontmatter.description === 'string' ? frontmatter.description : undefined,
|
|
212
|
+
dependencies,
|
|
213
|
+
createdAt: typeof frontmatter.created === 'string' ? frontmatter.created : undefined,
|
|
214
|
+
completedAt: typeof frontmatter.completed_at === 'string' ? frontmatter.completed_at : undefined
|
|
215
|
+
};
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function scanIntents(rootPath, normalizedState, warnings) {
|
|
220
|
+
const intentsPath = path.join(rootPath, 'intents');
|
|
221
|
+
const dirIntentIds = listSubdirectories(intentsPath);
|
|
222
|
+
const stateIntentIds = (normalizedState.intents || []).map((intent) => intent.id).filter(Boolean);
|
|
223
|
+
const uniqueIntentIds = Array.from(new Set([...dirIntentIds, ...stateIntentIds])).sort((a, b) => a.localeCompare(b));
|
|
224
|
+
|
|
225
|
+
const stateIntentMap = new Map((normalizedState.intents || []).map((intent) => [intent.id, intent]));
|
|
226
|
+
|
|
227
|
+
return uniqueIntentIds.map((intentId) => {
|
|
228
|
+
const stateIntent = stateIntentMap.get(intentId);
|
|
229
|
+
const intentPath = path.join(intentsPath, intentId);
|
|
230
|
+
const briefPath = path.join(intentPath, 'brief.md');
|
|
231
|
+
|
|
232
|
+
let frontmatter = {};
|
|
233
|
+
if (fs.existsSync(briefPath)) {
|
|
234
|
+
frontmatter = parseFrontmatter(readFileSafe(briefPath) || '');
|
|
235
|
+
} else if (stateIntent) {
|
|
236
|
+
warnings.push(`Intent ${intentId} exists in state.yaml but brief.md is missing.`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const workItems = scanWorkItems(
|
|
240
|
+
intentPath,
|
|
241
|
+
intentId,
|
|
242
|
+
stateIntent?.workItems || [],
|
|
243
|
+
warnings
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
id: intentId,
|
|
248
|
+
title: typeof frontmatter.title === 'string'
|
|
249
|
+
? frontmatter.title
|
|
250
|
+
: (stateIntent?.title || intentId),
|
|
251
|
+
status: deriveIntentStatus(stateIntent?.status, workItems),
|
|
252
|
+
filePath: briefPath,
|
|
253
|
+
description: typeof frontmatter.description === 'string' ? frontmatter.description : undefined,
|
|
254
|
+
workItems,
|
|
255
|
+
createdAt: typeof frontmatter.created === 'string' ? frontmatter.created : undefined,
|
|
256
|
+
completedAt: typeof frontmatter.completed_at === 'string' ? frontmatter.completed_at : undefined
|
|
257
|
+
};
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function scanRuns(rootPath, normalizedState) {
|
|
262
|
+
const runsPath = path.join(rootPath, 'runs');
|
|
263
|
+
const runDirs = listSubdirectories(runsPath).filter((name) => name.startsWith('run-'));
|
|
264
|
+
|
|
265
|
+
const stateActiveMap = new Map((normalizedState.runs?.active || []).map((run) => [run.id, run]));
|
|
266
|
+
const stateCompletedMap = new Map((normalizedState.runs?.completed || []).map((run) => [run.id, run]));
|
|
267
|
+
|
|
268
|
+
const runIds = Array.from(new Set([
|
|
269
|
+
...runDirs,
|
|
270
|
+
...Array.from(stateActiveMap.keys()),
|
|
271
|
+
...Array.from(stateCompletedMap.keys())
|
|
272
|
+
])).sort((a, b) => a.localeCompare(b));
|
|
273
|
+
|
|
274
|
+
return runIds.map((runId) => {
|
|
275
|
+
const folderPath = path.join(runsPath, runId);
|
|
276
|
+
const runLogPath = path.join(folderPath, 'run.md');
|
|
277
|
+
const parsedRunLog = parseRunLog(runLogPath);
|
|
278
|
+
|
|
279
|
+
const stateActiveRun = stateActiveMap.get(runId);
|
|
280
|
+
const stateCompletedRun = stateCompletedMap.get(runId);
|
|
281
|
+
|
|
282
|
+
const stateRunWorkItems = (stateActiveRun?.workItems && stateActiveRun.workItems.length > 0)
|
|
283
|
+
? stateActiveRun.workItems
|
|
284
|
+
: ((stateCompletedRun?.workItems && stateCompletedRun.workItems.length > 0)
|
|
285
|
+
? stateCompletedRun.workItems
|
|
286
|
+
: []);
|
|
287
|
+
const workItems = mergeRunWorkItems(
|
|
288
|
+
stateRunWorkItems.length > 0 ? stateRunWorkItems : parsedRunLog.workItems,
|
|
289
|
+
parsedRunLog.workItems
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
const completedAt = stateCompletedRun?.completed || parsedRunLog.completedAt || undefined;
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
id: runId,
|
|
296
|
+
scope: stateActiveRun?.scope || parsedRunLog.scope || 'single',
|
|
297
|
+
workItems,
|
|
298
|
+
currentItem: stateActiveRun?.currentItem || parsedRunLog.currentItem || null,
|
|
299
|
+
checkpointState: stateActiveRun?.checkpointState || parsedRunLog.checkpointState,
|
|
300
|
+
currentCheckpoint: stateActiveRun?.currentCheckpoint || parsedRunLog.currentCheckpoint,
|
|
301
|
+
folderPath,
|
|
302
|
+
startedAt: stateActiveRun?.started || parsedRunLog.startedAt || '',
|
|
303
|
+
completedAt: completedAt === 'null' ? undefined : completedAt,
|
|
304
|
+
hasPlan: fs.existsSync(path.join(folderPath, 'plan.md')),
|
|
305
|
+
hasWalkthrough: fs.existsSync(path.join(folderPath, 'walkthrough.md')),
|
|
306
|
+
hasTestReport: fs.existsSync(path.join(folderPath, 'test-report.md'))
|
|
307
|
+
};
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function scanStandards(rootPath) {
|
|
312
|
+
const standardsPath = path.join(rootPath, 'standards');
|
|
313
|
+
|
|
314
|
+
return STANDARD_TYPES
|
|
315
|
+
.filter((type) => fs.existsSync(path.join(standardsPath, `${type}.md`)))
|
|
316
|
+
.map((type) => ({
|
|
317
|
+
type,
|
|
318
|
+
filePath: path.join(standardsPath, `${type}.md`),
|
|
319
|
+
scope: 'root'
|
|
320
|
+
}));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function buildActiveRuns(runs, normalizedState) {
|
|
324
|
+
const byId = new Map((runs || []).map((run) => [run.id, run]));
|
|
325
|
+
|
|
326
|
+
return (normalizedState.runs?.active || [])
|
|
327
|
+
.map((active) => byId.get(active.id) || null)
|
|
328
|
+
.filter(Boolean);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function buildCompletedRuns(runs) {
|
|
332
|
+
return (runs || [])
|
|
333
|
+
.filter((run) => run.completedAt != null)
|
|
334
|
+
.sort((a, b) => {
|
|
335
|
+
const aTime = a.completedAt ? Date.parse(a.completedAt) : 0;
|
|
336
|
+
const bTime = b.completedAt ? Date.parse(b.completedAt) : 0;
|
|
337
|
+
if (bTime !== aTime) {
|
|
338
|
+
return bTime - aTime;
|
|
339
|
+
}
|
|
340
|
+
return b.id.localeCompare(a.id);
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function createUninitializedSnapshot(workspacePath, rootPath) {
|
|
345
|
+
return {
|
|
346
|
+
flow: 'fire',
|
|
347
|
+
isProject: true,
|
|
348
|
+
initialized: false,
|
|
349
|
+
workspacePath,
|
|
350
|
+
rootPath,
|
|
351
|
+
version: '0.0.0',
|
|
352
|
+
project: null,
|
|
353
|
+
workspace: null,
|
|
354
|
+
intents: [],
|
|
355
|
+
runs: [],
|
|
356
|
+
activeRuns: [],
|
|
357
|
+
completedRuns: [],
|
|
358
|
+
pendingItems: [],
|
|
359
|
+
standards: scanStandards(rootPath),
|
|
360
|
+
stats: {
|
|
361
|
+
totalIntents: 0,
|
|
362
|
+
completedIntents: 0,
|
|
363
|
+
inProgressIntents: 0,
|
|
364
|
+
pendingIntents: 0,
|
|
365
|
+
blockedIntents: 0,
|
|
366
|
+
totalWorkItems: 0,
|
|
367
|
+
completedWorkItems: 0,
|
|
368
|
+
inProgressWorkItems: 0,
|
|
369
|
+
pendingWorkItems: 0,
|
|
370
|
+
blockedWorkItems: 0,
|
|
371
|
+
totalRuns: 0,
|
|
372
|
+
completedRuns: 0,
|
|
373
|
+
activeRunsCount: 0
|
|
374
|
+
},
|
|
375
|
+
warnings: ['FIRE folder exists but state.yaml has not been created yet.'],
|
|
376
|
+
generatedAt: new Date().toISOString()
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function parseStateFile(statePath) {
|
|
381
|
+
const content = readFileSafe(statePath);
|
|
382
|
+
if (content == null) {
|
|
383
|
+
throw new Error('Unable to read state.yaml');
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const parsed = yaml.load(content);
|
|
387
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
388
|
+
throw new Error('state.yaml is empty or invalid.');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return parsed;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function parseFireDashboard(workspacePath) {
|
|
395
|
+
const rootPath = path.join(workspacePath, '.specs-fire');
|
|
396
|
+
|
|
397
|
+
if (!fs.existsSync(rootPath) || !fs.statSync(rootPath).isDirectory()) {
|
|
398
|
+
return {
|
|
399
|
+
ok: false,
|
|
400
|
+
error: {
|
|
401
|
+
code: 'FIRE_NOT_FOUND',
|
|
402
|
+
message: `No FIRE workspace found at ${rootPath}`,
|
|
403
|
+
hint: 'Install FIRE flow or run this command from a FIRE project root.'
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const statePath = path.join(rootPath, 'state.yaml');
|
|
409
|
+
if (!fs.existsSync(statePath)) {
|
|
410
|
+
return {
|
|
411
|
+
ok: true,
|
|
412
|
+
snapshot: createUninitializedSnapshot(workspacePath, rootPath)
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
let rawState;
|
|
417
|
+
try {
|
|
418
|
+
rawState = parseStateFile(statePath);
|
|
419
|
+
} catch (error) {
|
|
420
|
+
return {
|
|
421
|
+
ok: false,
|
|
422
|
+
error: {
|
|
423
|
+
code: 'STATE_PARSE_ERROR',
|
|
424
|
+
message: `Failed to parse ${statePath}`,
|
|
425
|
+
details: error.message,
|
|
426
|
+
path: statePath
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const warnings = [];
|
|
432
|
+
const normalizedState = normalizeState(rawState);
|
|
433
|
+
const intents = scanIntents(rootPath, normalizedState, warnings);
|
|
434
|
+
const runs = scanRuns(rootPath, normalizedState);
|
|
435
|
+
const activeRuns = buildActiveRuns(runs, normalizedState);
|
|
436
|
+
const completedRuns = buildCompletedRuns(runs);
|
|
437
|
+
const standards = scanStandards(rootPath);
|
|
438
|
+
const pendingItems = buildPendingItems(intents);
|
|
439
|
+
const stats = calculateStats(intents, runs, activeRuns);
|
|
440
|
+
|
|
441
|
+
return {
|
|
442
|
+
ok: true,
|
|
443
|
+
snapshot: {
|
|
444
|
+
flow: 'fire',
|
|
445
|
+
isProject: true,
|
|
446
|
+
initialized: true,
|
|
447
|
+
workspacePath,
|
|
448
|
+
rootPath,
|
|
449
|
+
version: normalizedState.project?.fireVersion || '0.0.0',
|
|
450
|
+
project: normalizedState.project,
|
|
451
|
+
workspace: normalizedState.workspace,
|
|
452
|
+
intents,
|
|
453
|
+
runs,
|
|
454
|
+
activeRuns,
|
|
455
|
+
completedRuns,
|
|
456
|
+
pendingItems,
|
|
457
|
+
standards,
|
|
458
|
+
stats,
|
|
459
|
+
warnings,
|
|
460
|
+
generatedAt: new Date().toISOString()
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
module.exports = {
|
|
466
|
+
STANDARD_TYPES,
|
|
467
|
+
parseFrontmatter,
|
|
468
|
+
parseRunLog,
|
|
469
|
+
parseFireDashboard
|
|
470
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const SUPPORTED_FLOWS = ['fire', 'aidlc', 'simple'];
|
|
5
|
+
|
|
6
|
+
function directoryExists(dirPath) {
|
|
7
|
+
try {
|
|
8
|
+
return fs.statSync(dirPath).isDirectory();
|
|
9
|
+
} catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getFlowMarkerPath(workspacePath, flow) {
|
|
15
|
+
switch (flow) {
|
|
16
|
+
case 'fire':
|
|
17
|
+
return path.join(workspacePath, '.specs-fire');
|
|
18
|
+
case 'aidlc':
|
|
19
|
+
return path.join(workspacePath, 'memory-bank');
|
|
20
|
+
case 'simple':
|
|
21
|
+
return path.join(workspacePath, 'specs');
|
|
22
|
+
default:
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function detectAvailableFlows(workspacePath) {
|
|
28
|
+
return SUPPORTED_FLOWS.filter((flow) => {
|
|
29
|
+
const markerPath = getFlowMarkerPath(workspacePath, flow);
|
|
30
|
+
return markerPath && directoryExists(markerPath);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function detectFlow(workspacePath, explicitFlow) {
|
|
35
|
+
const availableFlows = detectAvailableFlows(workspacePath);
|
|
36
|
+
|
|
37
|
+
if (explicitFlow) {
|
|
38
|
+
if (!SUPPORTED_FLOWS.includes(explicitFlow)) {
|
|
39
|
+
const valid = SUPPORTED_FLOWS.join(', ');
|
|
40
|
+
throw new Error(`Invalid flow \"${explicitFlow}\". Valid options: ${valid}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const markerPath = getFlowMarkerPath(workspacePath, explicitFlow);
|
|
44
|
+
const exists = markerPath ? directoryExists(markerPath) : false;
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
flow: explicitFlow,
|
|
48
|
+
source: 'flag',
|
|
49
|
+
markerPath,
|
|
50
|
+
detected: exists,
|
|
51
|
+
availableFlows,
|
|
52
|
+
warning: exists
|
|
53
|
+
? undefined
|
|
54
|
+
: `Flow \"${explicitFlow}\" was selected explicitly but ${markerPath} was not found.`
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const flow of SUPPORTED_FLOWS) {
|
|
59
|
+
const markerPath = getFlowMarkerPath(workspacePath, flow);
|
|
60
|
+
if (markerPath && directoryExists(markerPath)) {
|
|
61
|
+
return {
|
|
62
|
+
flow,
|
|
63
|
+
source: 'auto',
|
|
64
|
+
markerPath,
|
|
65
|
+
detected: true,
|
|
66
|
+
availableFlows
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
flow: null,
|
|
73
|
+
source: 'auto',
|
|
74
|
+
markerPath: null,
|
|
75
|
+
detected: false,
|
|
76
|
+
availableFlows
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = {
|
|
81
|
+
SUPPORTED_FLOWS,
|
|
82
|
+
directoryExists,
|
|
83
|
+
getFlowMarkerPath,
|
|
84
|
+
detectAvailableFlows,
|
|
85
|
+
detectFlow
|
|
86
|
+
};
|