specsmd 0.0.0-dev.85 → 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/builder/skills/walkthrough-generate/SKILL.md +30 -27
- 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/flows/ideation/agents/orchestrator/agent.md +8 -7
- package/flows/ideation/agents/orchestrator/skills/flame/SKILL.md +1 -0
- package/flows/ideation/agents/orchestrator/skills/flame/references/evaluation-criteria.md +4 -0
- package/flows/ideation/agents/orchestrator/skills/flame/references/six-hats-method.md +12 -0
- package/flows/ideation/agents/orchestrator/skills/forge/SKILL.md +1 -0
- package/flows/ideation/agents/orchestrator/skills/forge/references/disney-method.md +8 -0
- package/flows/ideation/agents/orchestrator/skills/forge/references/pitch-framework.md +15 -0
- package/flows/ideation/agents/orchestrator/skills/spark/SKILL.md +1 -0
- package/flows/ideation/agents/orchestrator/skills/spark/references/techniques/analogy.md +7 -0
- package/flows/ideation/agents/orchestrator/skills/spark/references/techniques/first-principles.md +5 -0
- package/flows/ideation/agents/orchestrator/skills/spark/references/techniques/inversion.md +6 -0
- package/flows/ideation/agents/orchestrator/skills/spark/references/techniques/questorming.md +6 -0
- package/flows/ideation/agents/orchestrator/skills/spark/references/techniques/random-word.md +1 -0
- package/flows/ideation/agents/orchestrator/skills/spark/references/techniques/scamper.md +15 -0
- package/flows/ideation/agents/orchestrator/skills/spark/references/techniques/what-if.md +6 -0
- package/flows/ideation/shared/protocols/anti-bias.md +7 -4
- package/flows/ideation/shared/protocols/deep-thinking.md +7 -0
- package/flows/ideation/shared/protocols/diverge-converge.md +2 -0
- package/flows/ideation/shared/protocols/interaction-adaptation.md +7 -0
- 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,797 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { spawnSync } = require('child_process');
|
|
3
|
+
const { truncate, normalizePanelLine, clampIndex, fileExists } = require('./helpers');
|
|
4
|
+
const { sanitizeRenderLine } = require('./overlays');
|
|
5
|
+
const {
|
|
6
|
+
getEffectiveFlow,
|
|
7
|
+
getCurrentRun,
|
|
8
|
+
getCurrentFireWorkItem,
|
|
9
|
+
getCurrentBolt,
|
|
10
|
+
getCurrentSpec,
|
|
11
|
+
getCurrentPhaseLabel,
|
|
12
|
+
buildPhaseTrack,
|
|
13
|
+
isFireRunAwaitingApproval,
|
|
14
|
+
isAidlcBoltAwaitingApproval
|
|
15
|
+
} = require('./flow-builders');
|
|
16
|
+
const {
|
|
17
|
+
collectFireRunFiles,
|
|
18
|
+
collectAidlcBoltFiles,
|
|
19
|
+
collectSimpleSpecFiles,
|
|
20
|
+
collectAidlcIntentContextFiles,
|
|
21
|
+
filterExistingFiles,
|
|
22
|
+
getRunFileEntries,
|
|
23
|
+
buildIntentScopedLabel,
|
|
24
|
+
findIntentIdForWorkItem,
|
|
25
|
+
resolveFireWorkItemPath,
|
|
26
|
+
formatScope,
|
|
27
|
+
getNoFileMessage,
|
|
28
|
+
getNoPendingMessage,
|
|
29
|
+
getNoCompletedMessage,
|
|
30
|
+
getNoCurrentMessage
|
|
31
|
+
} = require('./file-entries');
|
|
32
|
+
|
|
33
|
+
function buildFireCurrentRunGroups(snapshot) {
|
|
34
|
+
const run = getCurrentRun(snapshot);
|
|
35
|
+
if (!run) {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const workItems = Array.isArray(run.workItems) ? run.workItems : [];
|
|
40
|
+
const completed = workItems.filter((item) => item.status === 'completed').length;
|
|
41
|
+
const currentWorkItem = getCurrentFireWorkItem(run);
|
|
42
|
+
const awaitingApproval = isFireRunAwaitingApproval(run, currentWorkItem);
|
|
43
|
+
|
|
44
|
+
const currentPhase = getCurrentPhaseLabel(run, currentWorkItem);
|
|
45
|
+
const phaseTrack = buildPhaseTrack(currentPhase);
|
|
46
|
+
const mode = String(currentWorkItem?.mode || 'confirm').toUpperCase();
|
|
47
|
+
const status = currentWorkItem?.status || 'pending';
|
|
48
|
+
const statusTag = status === 'in_progress' ? 'current' : status;
|
|
49
|
+
|
|
50
|
+
const runIntentId = typeof run?.intent === 'string' ? run.intent : '';
|
|
51
|
+
const currentWorkItemFiles = workItems.map((item, index) => {
|
|
52
|
+
const itemId = typeof item?.id === 'string' && item.id !== '' ? item.id : `work-item-${index + 1}`;
|
|
53
|
+
const intentId = typeof item?.intent === 'string' && item.intent !== ''
|
|
54
|
+
? item.intent
|
|
55
|
+
: (runIntentId || findIntentIdForWorkItem(snapshot, itemId));
|
|
56
|
+
const filePath = resolveFireWorkItemPath(snapshot, intentId, itemId, item?.filePath);
|
|
57
|
+
if (!filePath) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const itemMode = String(item?.mode || 'confirm').toUpperCase();
|
|
62
|
+
const itemStatus = item?.status || 'pending';
|
|
63
|
+
const isCurrent = Boolean(currentWorkItem?.id) && itemId === currentWorkItem.id;
|
|
64
|
+
const itemScope = isCurrent
|
|
65
|
+
? 'active'
|
|
66
|
+
: (itemStatus === 'completed' ? 'completed' : 'upcoming');
|
|
67
|
+
const itemStatusTag = isCurrent ? 'current' : itemStatus;
|
|
68
|
+
const labelPath = buildIntentScopedLabel(snapshot, intentId, filePath, `${itemId}.md`);
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
label: `${labelPath} [${itemMode}] [${itemStatusTag}]`,
|
|
72
|
+
path: filePath,
|
|
73
|
+
scope: itemScope
|
|
74
|
+
};
|
|
75
|
+
}).filter(Boolean);
|
|
76
|
+
|
|
77
|
+
const currentRunFiles = collectFireRunFiles(run).map((fileEntry) => ({
|
|
78
|
+
...fileEntry,
|
|
79
|
+
label: path.basename(fileEntry.path || fileEntry.label || ''),
|
|
80
|
+
scope: 'active'
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
return [
|
|
84
|
+
{
|
|
85
|
+
key: `current:run:${run.id}:summary`,
|
|
86
|
+
label: `${run.id} [${run.scope}] ${completed}/${workItems.length} items${awaitingApproval ? ' [APPROVAL]' : ''}`,
|
|
87
|
+
files: []
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
key: `current:run:${run.id}:work-items`,
|
|
91
|
+
label: `WORK ITEMS (${currentWorkItemFiles.length})`,
|
|
92
|
+
files: filterExistingFiles(currentWorkItemFiles)
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
key: `current:run:${run.id}:run-files`,
|
|
96
|
+
label: 'RUN FILES',
|
|
97
|
+
files: filterExistingFiles(currentRunFiles)
|
|
98
|
+
}
|
|
99
|
+
];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function buildCurrentGroups(snapshot, flow) {
|
|
103
|
+
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
104
|
+
|
|
105
|
+
if (effectiveFlow === 'aidlc') {
|
|
106
|
+
const bolt = getCurrentBolt(snapshot);
|
|
107
|
+
if (!bolt) {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
const stages = Array.isArray(bolt.stages) ? bolt.stages : [];
|
|
111
|
+
const completedStages = stages.filter((stage) => stage.status === 'completed').length;
|
|
112
|
+
const awaitingApproval = isAidlcBoltAwaitingApproval(bolt);
|
|
113
|
+
return [{
|
|
114
|
+
key: `current:bolt:${bolt.id}`,
|
|
115
|
+
label: `${bolt.id} [${bolt.type}] ${completedStages}/${stages.length} stages${awaitingApproval ? ' [APPROVAL]' : ''}`,
|
|
116
|
+
files: filterExistingFiles([
|
|
117
|
+
...collectAidlcBoltFiles(bolt),
|
|
118
|
+
...collectAidlcIntentContextFiles(snapshot, bolt.intent)
|
|
119
|
+
])
|
|
120
|
+
}];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (effectiveFlow === 'simple') {
|
|
124
|
+
const spec = getCurrentSpec(snapshot);
|
|
125
|
+
if (!spec) {
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
return [{
|
|
129
|
+
key: `current:spec:${spec.name}`,
|
|
130
|
+
label: `${spec.name} [${spec.state}] ${spec.tasksCompleted}/${spec.tasksTotal} tasks`,
|
|
131
|
+
files: filterExistingFiles(collectSimpleSpecFiles(spec))
|
|
132
|
+
}];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return buildFireCurrentRunGroups(snapshot);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function buildRunFileGroups(fileEntries) {
|
|
139
|
+
const order = ['active', 'upcoming', 'completed', 'intent', 'other'];
|
|
140
|
+
const buckets = new Map(order.map((scope) => [scope, []]));
|
|
141
|
+
|
|
142
|
+
for (const fileEntry of Array.isArray(fileEntries) ? fileEntries : []) {
|
|
143
|
+
const scope = order.includes(fileEntry?.scope) ? fileEntry.scope : 'other';
|
|
144
|
+
buckets.get(scope).push(fileEntry);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const groups = [];
|
|
148
|
+
for (const scope of order) {
|
|
149
|
+
const files = buckets.get(scope) || [];
|
|
150
|
+
if (files.length === 0) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
groups.push({
|
|
154
|
+
key: `run-files:scope:${scope}`,
|
|
155
|
+
label: `${formatScope(scope)} files (${files.length})`,
|
|
156
|
+
files: filterExistingFiles(files)
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return groups;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function getFileEntityLabel(fileEntry, fallbackIndex = 0) {
|
|
163
|
+
const rawLabel = typeof fileEntry?.label === 'string' ? fileEntry.label : '';
|
|
164
|
+
if (rawLabel.includes('/')) {
|
|
165
|
+
return rawLabel.split('/')[0] || `item-${fallbackIndex + 1}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const filePath = typeof fileEntry?.path === 'string' ? fileEntry.path : '';
|
|
169
|
+
if (filePath !== '') {
|
|
170
|
+
const parentDir = path.basename(path.dirname(filePath));
|
|
171
|
+
if (parentDir && parentDir !== '.' && parentDir !== path.sep) {
|
|
172
|
+
return parentDir;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const baseName = path.basename(filePath);
|
|
176
|
+
if (baseName) {
|
|
177
|
+
return baseName;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return `item-${fallbackIndex + 1}`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function buildRunFileEntityGroups(snapshot, flow, options = {}) {
|
|
185
|
+
const order = ['active', 'upcoming', 'completed', 'intent', 'other'];
|
|
186
|
+
const rankByScope = new Map(order.map((scope, index) => [scope, index]));
|
|
187
|
+
const entries = filterExistingFiles(getRunFileEntries(snapshot, flow, options));
|
|
188
|
+
const groupsByEntity = new Map();
|
|
189
|
+
|
|
190
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
191
|
+
const fileEntry = entries[index];
|
|
192
|
+
const entity = getFileEntityLabel(fileEntry, index);
|
|
193
|
+
const scope = order.includes(fileEntry?.scope) ? fileEntry.scope : 'other';
|
|
194
|
+
const scopeRank = rankByScope.get(scope) ?? rankByScope.get('other');
|
|
195
|
+
|
|
196
|
+
if (!groupsByEntity.has(entity)) {
|
|
197
|
+
groupsByEntity.set(entity, {
|
|
198
|
+
entity,
|
|
199
|
+
files: [],
|
|
200
|
+
scope,
|
|
201
|
+
scopeRank
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const group = groupsByEntity.get(entity);
|
|
206
|
+
group.files.push(fileEntry);
|
|
207
|
+
|
|
208
|
+
if (scopeRank < group.scopeRank) {
|
|
209
|
+
group.scopeRank = scopeRank;
|
|
210
|
+
group.scope = scope;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return Array.from(groupsByEntity.values())
|
|
215
|
+
.sort((a, b) => {
|
|
216
|
+
if (a.scopeRank !== b.scopeRank) {
|
|
217
|
+
return a.scopeRank - b.scopeRank;
|
|
218
|
+
}
|
|
219
|
+
return String(a.entity).localeCompare(String(b.entity));
|
|
220
|
+
})
|
|
221
|
+
.map((group) => ({
|
|
222
|
+
key: `run-files:entity:${group.entity}`,
|
|
223
|
+
label: `${group.entity} [${formatScope(group.scope)}] (${group.files.length})`,
|
|
224
|
+
files: filterExistingFiles(group.files)
|
|
225
|
+
}))
|
|
226
|
+
.filter((group) => group.files.length > 0);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function normalizeInfoLine(line) {
|
|
230
|
+
const normalized = normalizePanelLine(line);
|
|
231
|
+
return {
|
|
232
|
+
label: normalized.text,
|
|
233
|
+
color: normalized.color,
|
|
234
|
+
bold: normalized.bold
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function toInfoRows(lines, keyPrefix, emptyLabel = 'No data') {
|
|
239
|
+
const safe = Array.isArray(lines) ? lines : [];
|
|
240
|
+
if (safe.length === 0) {
|
|
241
|
+
return [{
|
|
242
|
+
kind: 'info',
|
|
243
|
+
key: `${keyPrefix}:empty`,
|
|
244
|
+
label: emptyLabel,
|
|
245
|
+
selectable: false
|
|
246
|
+
}];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return safe.map((line, index) => {
|
|
250
|
+
const normalized = normalizeInfoLine(line);
|
|
251
|
+
return {
|
|
252
|
+
kind: 'info',
|
|
253
|
+
key: `${keyPrefix}:${index}`,
|
|
254
|
+
label: normalized.label,
|
|
255
|
+
color: normalized.color,
|
|
256
|
+
bold: normalized.bold,
|
|
257
|
+
selectable: true
|
|
258
|
+
};
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function toLoadingRows(label, keyPrefix = 'loading') {
|
|
263
|
+
return [{
|
|
264
|
+
kind: 'loading',
|
|
265
|
+
key: `${keyPrefix}:row`,
|
|
266
|
+
label: typeof label === 'string' && label !== '' ? label : 'Loading...',
|
|
267
|
+
selectable: false
|
|
268
|
+
}];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function buildOverviewIntentGroups(snapshot, flow, filter = 'next') {
|
|
272
|
+
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
273
|
+
const normalizedFilter = filter === 'completed' ? 'completed' : 'next';
|
|
274
|
+
const isIncluded = (status) => {
|
|
275
|
+
if (normalizedFilter === 'completed') {
|
|
276
|
+
return status === 'completed';
|
|
277
|
+
}
|
|
278
|
+
return status !== 'completed';
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
if (effectiveFlow === 'aidlc') {
|
|
282
|
+
const intents = Array.isArray(snapshot?.intents) ? snapshot.intents : [];
|
|
283
|
+
return intents
|
|
284
|
+
.filter((intent) => isIncluded(intent?.status || 'pending'))
|
|
285
|
+
.map((intent, index) => ({
|
|
286
|
+
key: `overview:intent:${intent?.id || index}`,
|
|
287
|
+
label: `${intent?.id || 'unknown'}: ${intent?.status || 'pending'} (${intent?.completedStories || 0}/${intent?.storyCount || 0} stories, ${intent?.completedUnits || 0}/${intent?.unitCount || 0} units)`,
|
|
288
|
+
files: filterExistingFiles(collectAidlcIntentContextFiles(snapshot, intent?.id))
|
|
289
|
+
}));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (effectiveFlow === 'simple') {
|
|
293
|
+
const specs = Array.isArray(snapshot?.specs) ? snapshot.specs : [];
|
|
294
|
+
return specs
|
|
295
|
+
.filter((spec) => isIncluded(spec?.state || 'pending'))
|
|
296
|
+
.map((spec, index) => ({
|
|
297
|
+
key: `overview:spec:${spec?.name || index}`,
|
|
298
|
+
label: `${spec?.name || 'unknown'}: ${spec?.state || 'pending'} (${spec?.tasksCompleted || 0}/${spec?.tasksTotal || 0} tasks)`,
|
|
299
|
+
files: filterExistingFiles(collectSimpleSpecFiles(spec))
|
|
300
|
+
}));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const intents = Array.isArray(snapshot?.intents) ? snapshot.intents : [];
|
|
304
|
+
return intents
|
|
305
|
+
.filter((intent) => isIncluded(intent?.status || 'pending'))
|
|
306
|
+
.map((intent, index) => {
|
|
307
|
+
const workItems = Array.isArray(intent?.workItems) ? intent.workItems : [];
|
|
308
|
+
const done = workItems.filter((item) => item.status === 'completed').length;
|
|
309
|
+
const files = [{
|
|
310
|
+
label: buildIntentScopedLabel(snapshot, intent?.id, intent?.filePath, 'brief.md'),
|
|
311
|
+
path: intent?.filePath,
|
|
312
|
+
scope: 'intent'
|
|
313
|
+
}, ...workItems.map((item) => ({
|
|
314
|
+
label: buildIntentScopedLabel(
|
|
315
|
+
snapshot,
|
|
316
|
+
intent?.id,
|
|
317
|
+
item?.filePath,
|
|
318
|
+
`${item?.id || 'work-item'}.md`
|
|
319
|
+
),
|
|
320
|
+
path: item?.filePath,
|
|
321
|
+
scope: item?.status === 'completed' ? 'completed' : 'upcoming'
|
|
322
|
+
}))];
|
|
323
|
+
return {
|
|
324
|
+
key: `overview:intent:${intent?.id || index}`,
|
|
325
|
+
label: `${intent?.id || 'unknown'}: ${intent?.status || 'pending'} (${done}/${workItems.length} work items)`,
|
|
326
|
+
files: filterExistingFiles(files)
|
|
327
|
+
};
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function buildStandardsRows(snapshot, flow) {
|
|
332
|
+
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
333
|
+
if (effectiveFlow === 'simple') {
|
|
334
|
+
return [{
|
|
335
|
+
kind: 'info',
|
|
336
|
+
key: 'standards:empty:simple',
|
|
337
|
+
label: 'No standards for SIMPLE flow',
|
|
338
|
+
selectable: false
|
|
339
|
+
}];
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const standards = Array.isArray(snapshot?.standards) ? snapshot.standards : [];
|
|
343
|
+
const files = filterExistingFiles(standards.map((standard, index) => ({
|
|
344
|
+
label: `${standard?.name || standard?.type || `standard-${index}`}.md`,
|
|
345
|
+
path: standard?.filePath,
|
|
346
|
+
scope: 'file'
|
|
347
|
+
})));
|
|
348
|
+
|
|
349
|
+
if (files.length === 0) {
|
|
350
|
+
return [{
|
|
351
|
+
kind: 'info',
|
|
352
|
+
key: 'standards:empty',
|
|
353
|
+
label: 'No standards found',
|
|
354
|
+
selectable: false
|
|
355
|
+
}];
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return files.map((file, index) => ({
|
|
359
|
+
kind: 'file',
|
|
360
|
+
key: `standards:file:${file.path}:${index}`,
|
|
361
|
+
label: file.label,
|
|
362
|
+
path: file.path,
|
|
363
|
+
scope: 'file',
|
|
364
|
+
selectable: true
|
|
365
|
+
}));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function buildProjectGroups(snapshot, flow) {
|
|
369
|
+
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
370
|
+
const files = [];
|
|
371
|
+
|
|
372
|
+
if (effectiveFlow === 'aidlc') {
|
|
373
|
+
files.push({
|
|
374
|
+
label: 'memory-bank/project.yaml',
|
|
375
|
+
path: path.join(snapshot?.rootPath || '', 'project.yaml'),
|
|
376
|
+
scope: 'file'
|
|
377
|
+
});
|
|
378
|
+
} else if (effectiveFlow === 'simple') {
|
|
379
|
+
files.push({
|
|
380
|
+
label: 'package.json',
|
|
381
|
+
path: path.join(snapshot?.workspacePath || '', 'package.json'),
|
|
382
|
+
scope: 'file'
|
|
383
|
+
});
|
|
384
|
+
} else {
|
|
385
|
+
files.push({
|
|
386
|
+
label: '.specs-fire/state.yaml',
|
|
387
|
+
path: path.join(snapshot?.rootPath || '', 'state.yaml'),
|
|
388
|
+
scope: 'file'
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const projectName = snapshot?.project?.name || 'unknown-project';
|
|
393
|
+
return [{
|
|
394
|
+
key: `project:${projectName}`,
|
|
395
|
+
label: `project ${projectName}`,
|
|
396
|
+
files: filterExistingFiles(files)
|
|
397
|
+
}];
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function buildPendingGroups(snapshot, flow) {
|
|
401
|
+
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
402
|
+
|
|
403
|
+
if (effectiveFlow === 'aidlc') {
|
|
404
|
+
const pendingBolts = Array.isArray(snapshot?.pendingBolts) ? snapshot.pendingBolts : [];
|
|
405
|
+
return pendingBolts.map((bolt, index) => {
|
|
406
|
+
const deps = Array.isArray(bolt?.blockedBy) && bolt.blockedBy.length > 0
|
|
407
|
+
? ` blocked_by:${bolt.blockedBy.join(',')}`
|
|
408
|
+
: '';
|
|
409
|
+
const location = `${bolt?.intent || 'unknown'}/${bolt?.unit || 'unknown'}`;
|
|
410
|
+
const boltFiles = collectAidlcBoltFiles(bolt);
|
|
411
|
+
const intentFiles = collectAidlcIntentContextFiles(snapshot, bolt?.intent);
|
|
412
|
+
return {
|
|
413
|
+
key: `pending:bolt:${bolt?.id || index}`,
|
|
414
|
+
label: `${bolt?.id || 'unknown'} (${bolt?.status || 'pending'}) in ${location}${deps}`,
|
|
415
|
+
files: filterExistingFiles([...boltFiles, ...intentFiles])
|
|
416
|
+
};
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (effectiveFlow === 'simple') {
|
|
421
|
+
const pendingSpecs = Array.isArray(snapshot?.pendingSpecs) ? snapshot.pendingSpecs : [];
|
|
422
|
+
return pendingSpecs.map((spec, index) => ({
|
|
423
|
+
key: `pending:spec:${spec?.name || index}`,
|
|
424
|
+
label: `${spec?.name || 'unknown'} (${spec?.state || 'pending'}) ${spec?.tasksCompleted || 0}/${spec?.tasksTotal || 0} tasks`,
|
|
425
|
+
files: filterExistingFiles(collectSimpleSpecFiles(spec))
|
|
426
|
+
}));
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const pendingItems = Array.isArray(snapshot?.pendingItems) ? snapshot.pendingItems : [];
|
|
430
|
+
return pendingItems.map((item, index) => {
|
|
431
|
+
const deps = Array.isArray(item?.dependencies) && item.dependencies.length > 0
|
|
432
|
+
? ` deps:${item.dependencies.join(',')}`
|
|
433
|
+
: '';
|
|
434
|
+
const intentTitle = item?.intentTitle || item?.intentId || 'unknown-intent';
|
|
435
|
+
const files = [];
|
|
436
|
+
|
|
437
|
+
if (item?.filePath) {
|
|
438
|
+
files.push({
|
|
439
|
+
label: buildIntentScopedLabel(
|
|
440
|
+
snapshot,
|
|
441
|
+
item?.intentId,
|
|
442
|
+
item?.filePath,
|
|
443
|
+
`${item?.id || 'work-item'}.md`
|
|
444
|
+
),
|
|
445
|
+
path: item.filePath,
|
|
446
|
+
scope: 'upcoming'
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
if (item?.intentId) {
|
|
450
|
+
files.push({
|
|
451
|
+
label: buildIntentScopedLabel(
|
|
452
|
+
snapshot,
|
|
453
|
+
item.intentId,
|
|
454
|
+
path.join(snapshot?.rootPath || '', 'intents', item.intentId, 'brief.md'),
|
|
455
|
+
'brief.md'
|
|
456
|
+
),
|
|
457
|
+
path: path.join(snapshot?.rootPath || '', 'intents', item.intentId, 'brief.md'),
|
|
458
|
+
scope: 'intent'
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return {
|
|
463
|
+
key: `pending:item:${item?.intentId || 'intent'}:${item?.id || index}`,
|
|
464
|
+
label: `${item?.id || 'work-item'} (${item?.mode || 'confirm'}/${item?.complexity || 'medium'}) in ${intentTitle}${deps}`,
|
|
465
|
+
files: filterExistingFiles(files)
|
|
466
|
+
};
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function buildCompletedGroups(snapshot, flow) {
|
|
471
|
+
const effectiveFlow = getEffectiveFlow(flow, snapshot);
|
|
472
|
+
|
|
473
|
+
if (effectiveFlow === 'aidlc') {
|
|
474
|
+
const completedBolts = Array.isArray(snapshot?.completedBolts) ? snapshot.completedBolts : [];
|
|
475
|
+
return completedBolts.map((bolt, index) => {
|
|
476
|
+
const boltFiles = collectAidlcBoltFiles(bolt);
|
|
477
|
+
const intentFiles = collectAidlcIntentContextFiles(snapshot, bolt?.intent);
|
|
478
|
+
return {
|
|
479
|
+
key: `completed:bolt:${bolt?.id || index}`,
|
|
480
|
+
label: `${bolt?.id || 'unknown'} [${bolt?.type || 'bolt'}] done at ${bolt?.completedAt || 'unknown'}`,
|
|
481
|
+
files: filterExistingFiles([...boltFiles, ...intentFiles])
|
|
482
|
+
};
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (effectiveFlow === 'simple') {
|
|
487
|
+
const completedSpecs = Array.isArray(snapshot?.completedSpecs) ? snapshot.completedSpecs : [];
|
|
488
|
+
return completedSpecs.map((spec, index) => ({
|
|
489
|
+
key: `completed:spec:${spec?.name || index}`,
|
|
490
|
+
label: `${spec?.name || 'unknown'} done at ${spec?.updatedAt || 'unknown'} (${spec?.tasksCompleted || 0}/${spec?.tasksTotal || 0})`,
|
|
491
|
+
files: filterExistingFiles(collectSimpleSpecFiles(spec))
|
|
492
|
+
}));
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const groups = [];
|
|
496
|
+
const completedRuns = Array.isArray(snapshot?.completedRuns) ? snapshot.completedRuns : [];
|
|
497
|
+
for (let index = 0; index < completedRuns.length; index += 1) {
|
|
498
|
+
const run = completedRuns[index];
|
|
499
|
+
const workItems = Array.isArray(run?.workItems) ? run.workItems : [];
|
|
500
|
+
const completed = workItems.filter((item) => item.status === 'completed').length;
|
|
501
|
+
groups.push({
|
|
502
|
+
key: `completed:run:${run?.id || index}`,
|
|
503
|
+
label: `${run?.id || 'run'} [${run?.scope || 'single'}] ${completed}/${workItems.length} done at ${run?.completedAt || 'unknown'}`,
|
|
504
|
+
files: filterExistingFiles(collectFireRunFiles(run).map((file) => ({ ...file, scope: 'completed' })))
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const completedIntents = Array.isArray(snapshot?.intents)
|
|
509
|
+
? snapshot.intents.filter((intent) => intent?.status === 'completed')
|
|
510
|
+
: [];
|
|
511
|
+
for (let index = 0; index < completedIntents.length; index += 1) {
|
|
512
|
+
const intent = completedIntents[index];
|
|
513
|
+
groups.push({
|
|
514
|
+
key: `completed:intent:${intent?.id || index}`,
|
|
515
|
+
label: `intent ${intent?.id || 'unknown'} [completed]`,
|
|
516
|
+
files: filterExistingFiles([{
|
|
517
|
+
label: buildIntentScopedLabel(
|
|
518
|
+
snapshot,
|
|
519
|
+
intent?.id,
|
|
520
|
+
path.join(snapshot?.rootPath || '', 'intents', intent?.id || '', 'brief.md'),
|
|
521
|
+
'brief.md'
|
|
522
|
+
),
|
|
523
|
+
path: path.join(snapshot?.rootPath || '', 'intents', intent?.id || '', 'brief.md'),
|
|
524
|
+
scope: 'intent'
|
|
525
|
+
}])
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return groups;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function toExpandableRows(groups, emptyLabel, expandedGroups) {
|
|
533
|
+
if (!Array.isArray(groups) || groups.length === 0) {
|
|
534
|
+
return [{
|
|
535
|
+
kind: 'info',
|
|
536
|
+
key: 'section:empty',
|
|
537
|
+
label: emptyLabel,
|
|
538
|
+
selectable: false
|
|
539
|
+
}];
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const rows = [];
|
|
543
|
+
|
|
544
|
+
for (const group of groups) {
|
|
545
|
+
const files = filterExistingFiles(group?.files);
|
|
546
|
+
const expandable = files.length > 0;
|
|
547
|
+
const expanded = expandable && Boolean(expandedGroups?.[group.key]);
|
|
548
|
+
|
|
549
|
+
rows.push({
|
|
550
|
+
kind: 'group',
|
|
551
|
+
key: group.key,
|
|
552
|
+
label: group.label,
|
|
553
|
+
expandable,
|
|
554
|
+
expanded,
|
|
555
|
+
selectable: true
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
if (expanded) {
|
|
559
|
+
for (let index = 0; index < files.length; index += 1) {
|
|
560
|
+
const file = files[index];
|
|
561
|
+
rows.push({
|
|
562
|
+
kind: file.previewType === 'git-diff' ? 'git-file' : 'file',
|
|
563
|
+
key: `${group.key}:file:${file.path}:${index}`,
|
|
564
|
+
label: file.label,
|
|
565
|
+
path: file.path,
|
|
566
|
+
scope: file.scope || 'file',
|
|
567
|
+
selectable: true,
|
|
568
|
+
previewType: file.previewType,
|
|
569
|
+
repoRoot: file.repoRoot,
|
|
570
|
+
relativePath: file.relativePath,
|
|
571
|
+
bucket: file.bucket
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return rows;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function buildInteractiveRowsLines(rows, selectedIndex, icons, width, isFocusedSection) {
|
|
581
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
582
|
+
return [{ text: '', color: undefined, bold: false, selected: false }];
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const clampedIndex = clampIndex(selectedIndex, rows.length);
|
|
586
|
+
|
|
587
|
+
return rows.map((row, index) => {
|
|
588
|
+
const selectable = row?.selectable !== false;
|
|
589
|
+
const isSelected = selectable && index === clampedIndex;
|
|
590
|
+
const cursor = isSelected
|
|
591
|
+
? (isFocusedSection ? (icons.activeFile || '>') : '•')
|
|
592
|
+
: ' ';
|
|
593
|
+
|
|
594
|
+
if (row.kind === 'group') {
|
|
595
|
+
const marker = row.expandable
|
|
596
|
+
? (row.expanded ? (icons.groupExpanded || 'v') : (icons.groupCollapsed || '>'))
|
|
597
|
+
: '-';
|
|
598
|
+
const safeLabel = sanitizeRenderLine(row.label || '');
|
|
599
|
+
return {
|
|
600
|
+
text: truncate(`${cursor} ${marker} ${safeLabel}`, width),
|
|
601
|
+
color: isSelected ? (isFocusedSection ? 'green' : 'cyan') : undefined,
|
|
602
|
+
bold: isSelected,
|
|
603
|
+
selected: isSelected
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (row.kind === 'file' || row.kind === 'git-file' || row.kind === 'git-commit') {
|
|
608
|
+
const scope = row.scope ? `[${formatScope(row.scope)}] ` : '';
|
|
609
|
+
const safeLabel = sanitizeRenderLine(row.label || '');
|
|
610
|
+
return {
|
|
611
|
+
text: truncate(`${cursor} ${icons.runFile} ${scope}${safeLabel}`, width),
|
|
612
|
+
color: isSelected ? (isFocusedSection ? 'green' : 'cyan') : 'gray',
|
|
613
|
+
bold: isSelected,
|
|
614
|
+
selected: isSelected
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (row.kind === 'loading') {
|
|
619
|
+
return {
|
|
620
|
+
text: truncate(row.label || 'Loading...', width),
|
|
621
|
+
color: 'cyan',
|
|
622
|
+
bold: false,
|
|
623
|
+
selected: false,
|
|
624
|
+
loading: true
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return {
|
|
629
|
+
text: truncate(`${isSelected ? `${cursor} ` : ' '}${sanitizeRenderLine(row.label || '')}`, width),
|
|
630
|
+
color: isSelected ? (isFocusedSection ? 'green' : 'cyan') : (row.color || 'gray'),
|
|
631
|
+
bold: isSelected || Boolean(row.bold),
|
|
632
|
+
selected: isSelected
|
|
633
|
+
};
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function getSelectedRow(rows, selectedIndex) {
|
|
638
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
639
|
+
return null;
|
|
640
|
+
}
|
|
641
|
+
return rows[clampIndex(selectedIndex, rows.length)] || null;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function rowToFileEntry(row) {
|
|
645
|
+
if (!row) {
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (row.kind === 'git-commit') {
|
|
650
|
+
const commitHash = typeof row.commitHash === 'string' ? row.commitHash : '';
|
|
651
|
+
if (commitHash === '') {
|
|
652
|
+
return null;
|
|
653
|
+
}
|
|
654
|
+
return {
|
|
655
|
+
label: row.label || commitHash,
|
|
656
|
+
path: commitHash,
|
|
657
|
+
scope: 'commit',
|
|
658
|
+
previewType: row.previewType || 'git-commit-diff',
|
|
659
|
+
repoRoot: row.repoRoot,
|
|
660
|
+
commitHash
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if ((row.kind !== 'file' && row.kind !== 'git-file') || typeof row.path !== 'string') {
|
|
665
|
+
return null;
|
|
666
|
+
}
|
|
667
|
+
return {
|
|
668
|
+
label: row.label || path.basename(row.path),
|
|
669
|
+
path: row.path,
|
|
670
|
+
scope: row.scope || 'file',
|
|
671
|
+
previewType: row.previewType,
|
|
672
|
+
repoRoot: row.repoRoot,
|
|
673
|
+
relativePath: row.relativePath,
|
|
674
|
+
bucket: row.bucket
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function firstFileEntryFromRows(rows) {
|
|
679
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
680
|
+
return null;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
for (const row of rows) {
|
|
684
|
+
const entry = rowToFileEntry(row);
|
|
685
|
+
if (entry) {
|
|
686
|
+
return entry;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
return null;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function rowToWorktreeId(row) {
|
|
694
|
+
if (!row || typeof row.key !== 'string') {
|
|
695
|
+
return null;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const prefix = 'worktree:item:';
|
|
699
|
+
if (!row.key.startsWith(prefix)) {
|
|
700
|
+
return null;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const worktreeId = row.key.slice(prefix.length).trim();
|
|
704
|
+
return worktreeId === '' ? null : worktreeId;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function moveRowSelection(rows, currentIndex, direction) {
|
|
708
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
709
|
+
return 0;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const clamped = clampIndex(currentIndex, rows.length);
|
|
713
|
+
const step = direction >= 0 ? 1 : -1;
|
|
714
|
+
let next = clamped + step;
|
|
715
|
+
|
|
716
|
+
while (next >= 0 && next < rows.length) {
|
|
717
|
+
if (rows[next]?.selectable !== false) {
|
|
718
|
+
return next;
|
|
719
|
+
}
|
|
720
|
+
next += step;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
return clamped;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function openFileWithDefaultApp(filePath) {
|
|
727
|
+
if (typeof filePath !== 'string' || filePath.trim() === '') {
|
|
728
|
+
return {
|
|
729
|
+
ok: false,
|
|
730
|
+
message: 'No file selected to open.'
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
if (!fileExists(filePath)) {
|
|
735
|
+
return {
|
|
736
|
+
ok: false,
|
|
737
|
+
message: `File not found: ${filePath}`
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
let command = null;
|
|
742
|
+
let args = [];
|
|
743
|
+
|
|
744
|
+
if (process.platform === 'darwin') {
|
|
745
|
+
command = 'open';
|
|
746
|
+
args = [filePath];
|
|
747
|
+
} else if (process.platform === 'win32') {
|
|
748
|
+
command = 'cmd';
|
|
749
|
+
args = ['/c', 'start', '', filePath];
|
|
750
|
+
} else {
|
|
751
|
+
command = 'xdg-open';
|
|
752
|
+
args = [filePath];
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const result = spawnSync(command, args, { stdio: 'ignore' });
|
|
756
|
+
if (result.error) {
|
|
757
|
+
return {
|
|
758
|
+
ok: false,
|
|
759
|
+
message: `Unable to open file: ${result.error.message}`
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
if (typeof result.status === 'number' && result.status !== 0) {
|
|
763
|
+
return {
|
|
764
|
+
ok: false,
|
|
765
|
+
message: `Open command failed with exit code ${result.status}.`
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
return {
|
|
770
|
+
ok: true,
|
|
771
|
+
message: `Opened ${filePath}`
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
module.exports = {
|
|
776
|
+
buildFireCurrentRunGroups,
|
|
777
|
+
buildCurrentGroups,
|
|
778
|
+
buildRunFileGroups,
|
|
779
|
+
getFileEntityLabel,
|
|
780
|
+
buildRunFileEntityGroups,
|
|
781
|
+
normalizeInfoLine,
|
|
782
|
+
toInfoRows,
|
|
783
|
+
toLoadingRows,
|
|
784
|
+
buildOverviewIntentGroups,
|
|
785
|
+
buildStandardsRows,
|
|
786
|
+
buildProjectGroups,
|
|
787
|
+
buildPendingGroups,
|
|
788
|
+
buildCompletedGroups,
|
|
789
|
+
toExpandableRows,
|
|
790
|
+
buildInteractiveRowsLines,
|
|
791
|
+
getSelectedRow,
|
|
792
|
+
rowToFileEntry,
|
|
793
|
+
firstFileEntryFromRows,
|
|
794
|
+
rowToWorktreeId,
|
|
795
|
+
moveRowSelection,
|
|
796
|
+
openFileWithDefaultApp
|
|
797
|
+
};
|