specsmd 0.1.46 → 0.1.47
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/bin/cli.js +1 -0
- package/lib/dashboard/git/worktrees.js +248 -0
- package/lib/dashboard/index.js +473 -7
- package/lib/dashboard/runtime/watch-runtime.js +18 -9
- package/lib/dashboard/tui/app.js +423 -29
- package/package.json +1 -1
package/lib/dashboard/index.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
1
2
|
const path = require('path');
|
|
2
|
-
const
|
|
3
|
+
const yaml = require('js-yaml');
|
|
4
|
+
const { detectFlow, detectAvailableFlows } = require('./flow-detect');
|
|
3
5
|
const { parseFireDashboard } = require('./fire/parser');
|
|
4
6
|
const { parseAidlcDashboard } = require('./aidlc/parser');
|
|
5
7
|
const { parseSimpleDashboard } = require('./simple/parser');
|
|
6
8
|
const { formatDashboardText } = require('./tui/renderer');
|
|
7
9
|
const { createDashboardApp } = require('./tui/app');
|
|
10
|
+
const { discoverGitWorktrees, pickWorktree, pathExistsAsDirectory } = require('./git/worktrees');
|
|
8
11
|
|
|
9
12
|
function parseRefreshMs(raw) {
|
|
10
13
|
const parsed = Number.parseInt(String(raw || '1000'), 10);
|
|
@@ -66,6 +69,8 @@ const FLOW_CONFIG = {
|
|
|
66
69
|
}
|
|
67
70
|
};
|
|
68
71
|
|
|
72
|
+
const MAX_WORKTREE_WATCH_ROOTS = 12;
|
|
73
|
+
|
|
69
74
|
function resolveRootPathForFlow(workspacePath, flow) {
|
|
70
75
|
const config = FLOW_CONFIG[flow];
|
|
71
76
|
if (!config) {
|
|
@@ -74,6 +79,316 @@ function resolveRootPathForFlow(workspacePath, flow) {
|
|
|
74
79
|
return path.join(workspacePath, config.markerDir);
|
|
75
80
|
}
|
|
76
81
|
|
|
82
|
+
function readFileSafe(filePath) {
|
|
83
|
+
try {
|
|
84
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function parseFrontmatter(content) {
|
|
91
|
+
const match = String(content || '').match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
92
|
+
if (!match) {
|
|
93
|
+
return {};
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
const parsed = yaml.load(match[1]);
|
|
97
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
98
|
+
} catch {
|
|
99
|
+
return {};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function listSubdirectories(dirPath) {
|
|
104
|
+
try {
|
|
105
|
+
return fs.readdirSync(dirPath, { withFileTypes: true })
|
|
106
|
+
.filter((entry) => entry.isDirectory())
|
|
107
|
+
.map((entry) => entry.name)
|
|
108
|
+
.sort((a, b) => a.localeCompare(b));
|
|
109
|
+
} catch {
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function listMarkdownFiles(dirPath) {
|
|
115
|
+
try {
|
|
116
|
+
return fs.readdirSync(dirPath, { withFileTypes: true })
|
|
117
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
|
|
118
|
+
.map((entry) => entry.name)
|
|
119
|
+
.sort((a, b) => a.localeCompare(b));
|
|
120
|
+
} catch {
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function normalizeAidlcStatus(rawStatus) {
|
|
126
|
+
if (typeof rawStatus !== 'string') {
|
|
127
|
+
return 'unknown';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const normalized = rawStatus.toLowerCase().trim().replace(/[\s_]+/g, '-');
|
|
131
|
+
if (['complete', 'completed', 'done', 'finished', 'closed', 'resolved'].includes(normalized)) {
|
|
132
|
+
return 'completed';
|
|
133
|
+
}
|
|
134
|
+
if (['blocked'].includes(normalized)) {
|
|
135
|
+
return 'blocked';
|
|
136
|
+
}
|
|
137
|
+
if (['in-progress', 'inprogress', 'active', 'started', 'wip', 'working', 'ready', 'construction'].includes(normalized)) {
|
|
138
|
+
return 'in_progress';
|
|
139
|
+
}
|
|
140
|
+
if (['draft', 'pending', 'planned', 'todo', 'new', 'queued'].includes(normalized)) {
|
|
141
|
+
return 'pending';
|
|
142
|
+
}
|
|
143
|
+
return 'unknown';
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function normalizeFlowId(flow) {
|
|
147
|
+
return String(flow || '').toLowerCase().trim();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function normalizeFireRunWorkItem(raw) {
|
|
151
|
+
if (!raw || typeof raw !== 'object') {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
const id = typeof raw.id === 'string' ? raw.id : '';
|
|
155
|
+
if (id === '') {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
id,
|
|
160
|
+
intentId: typeof raw.intent === 'string' ? raw.intent : (typeof raw.intentId === 'string' ? raw.intentId : ''),
|
|
161
|
+
mode: typeof raw.mode === 'string' ? raw.mode : 'confirm',
|
|
162
|
+
status: typeof raw.status === 'string' ? raw.status : 'pending',
|
|
163
|
+
currentPhase: typeof raw.current_phase === 'string' ? raw.current_phase : (typeof raw.currentPhase === 'string' ? raw.currentPhase : undefined),
|
|
164
|
+
checkpointState: typeof raw.checkpoint_state === 'string'
|
|
165
|
+
? raw.checkpoint_state
|
|
166
|
+
: (typeof raw.checkpointState === 'string'
|
|
167
|
+
? raw.checkpointState
|
|
168
|
+
: undefined),
|
|
169
|
+
currentCheckpoint: typeof raw.current_checkpoint === 'string'
|
|
170
|
+
? raw.current_checkpoint
|
|
171
|
+
: (typeof raw.currentCheckpoint === 'string' ? raw.currentCheckpoint : undefined)
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function probeFireWorktreeActivity(worktreePath) {
|
|
176
|
+
const rootPath = path.join(worktreePath, '.specs-fire');
|
|
177
|
+
const statePath = path.join(rootPath, 'state.yaml');
|
|
178
|
+
const stateContent = readFileSafe(statePath);
|
|
179
|
+
if (stateContent == null) {
|
|
180
|
+
return { activeRuns: [] };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
let state;
|
|
184
|
+
try {
|
|
185
|
+
state = yaml.load(stateContent) || {};
|
|
186
|
+
} catch {
|
|
187
|
+
return { activeRuns: [] };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const activeRuns = Array.isArray(state?.runs?.active) ? state.runs.active : [];
|
|
191
|
+
return {
|
|
192
|
+
activeRuns: activeRuns.map((run) => {
|
|
193
|
+
const runId = typeof run?.id === 'string' ? run.id : '';
|
|
194
|
+
if (runId === '') {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
const folderPath = path.join(rootPath, 'runs', runId);
|
|
198
|
+
const workItemsRaw = Array.isArray(run?.work_items)
|
|
199
|
+
? run.work_items
|
|
200
|
+
: (Array.isArray(run?.workItems) ? run.workItems : []);
|
|
201
|
+
const workItems = workItemsRaw.map((item) => normalizeFireRunWorkItem(item)).filter(Boolean);
|
|
202
|
+
return {
|
|
203
|
+
id: runId,
|
|
204
|
+
scope: typeof run?.scope === 'string' ? run.scope : 'single',
|
|
205
|
+
currentItem: typeof run?.current_item === 'string'
|
|
206
|
+
? run.current_item
|
|
207
|
+
: (typeof run?.currentItem === 'string' ? run.currentItem : ''),
|
|
208
|
+
workItems,
|
|
209
|
+
startedAt: typeof run?.started === 'string' ? run.started : '',
|
|
210
|
+
folderPath,
|
|
211
|
+
hasPlan: fs.existsSync(path.join(folderPath, 'plan.md')),
|
|
212
|
+
hasWalkthrough: fs.existsSync(path.join(folderPath, 'walkthrough.md')),
|
|
213
|
+
hasTestReport: fs.existsSync(path.join(folderPath, 'test-report.md'))
|
|
214
|
+
};
|
|
215
|
+
}).filter(Boolean)
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function probeAidlcWorktreeActivity(worktreePath) {
|
|
220
|
+
const rootPath = path.join(worktreePath, 'memory-bank');
|
|
221
|
+
const boltsPath = path.join(rootPath, 'bolts');
|
|
222
|
+
const boltIds = listSubdirectories(boltsPath);
|
|
223
|
+
if (boltIds.length === 0) {
|
|
224
|
+
return { activeBolts: [] };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const activeBolts = boltIds.map((boltId) => {
|
|
228
|
+
const boltPath = path.join(boltsPath, boltId);
|
|
229
|
+
const boltFilePath = path.join(boltPath, 'bolt.md');
|
|
230
|
+
const content = readFileSafe(boltFilePath);
|
|
231
|
+
if (content == null) {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const frontmatter = parseFrontmatter(content);
|
|
236
|
+
const status = normalizeAidlcStatus(frontmatter.status);
|
|
237
|
+
if (status !== 'in_progress') {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const currentStage = typeof frontmatter.current_stage === 'string'
|
|
242
|
+
? frontmatter.current_stage
|
|
243
|
+
: (typeof frontmatter.currentStage === 'string' ? frontmatter.currentStage : null);
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
id: boltId,
|
|
247
|
+
intent: typeof frontmatter.intent === 'string' ? frontmatter.intent : '',
|
|
248
|
+
unit: typeof frontmatter.unit === 'string' ? frontmatter.unit : '',
|
|
249
|
+
type: typeof frontmatter.type === 'string' ? frontmatter.type : 'simple-construction-bolt',
|
|
250
|
+
status,
|
|
251
|
+
currentStage,
|
|
252
|
+
path: boltPath,
|
|
253
|
+
filePath: boltFilePath,
|
|
254
|
+
files: listMarkdownFiles(boltPath),
|
|
255
|
+
startedAt: typeof frontmatter.started === 'string' ? frontmatter.started : undefined
|
|
256
|
+
};
|
|
257
|
+
}).filter(Boolean);
|
|
258
|
+
|
|
259
|
+
return { activeBolts };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function probeSimpleWorktreeActivity(worktreePath) {
|
|
263
|
+
const rootPath = path.join(worktreePath, 'specs');
|
|
264
|
+
if (!pathExistsAsDirectory(rootPath)) {
|
|
265
|
+
return { activeSpecs: [] };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const specFolders = listSubdirectories(rootPath);
|
|
269
|
+
const activeSpecs = specFolders.map((name) => {
|
|
270
|
+
const specPath = path.join(rootPath, name);
|
|
271
|
+
const tasksPath = path.join(specPath, 'tasks.md');
|
|
272
|
+
if (!fs.existsSync(tasksPath)) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
name,
|
|
277
|
+
path: specPath
|
|
278
|
+
};
|
|
279
|
+
}).filter(Boolean);
|
|
280
|
+
|
|
281
|
+
return { activeSpecs };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function probeWorktreeActivity(flow, worktreePath) {
|
|
285
|
+
const normalizedFlow = normalizeFlowId(flow);
|
|
286
|
+
if (normalizedFlow === 'aidlc') {
|
|
287
|
+
return probeAidlcWorktreeActivity(worktreePath);
|
|
288
|
+
}
|
|
289
|
+
if (normalizedFlow === 'simple') {
|
|
290
|
+
return probeSimpleWorktreeActivity(worktreePath);
|
|
291
|
+
}
|
|
292
|
+
return probeFireWorktreeActivity(worktreePath);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function extractActivityFromSnapshot(flow, snapshot) {
|
|
296
|
+
const normalizedFlow = normalizeFlowId(flow);
|
|
297
|
+
if (normalizedFlow === 'aidlc') {
|
|
298
|
+
return {
|
|
299
|
+
activeBolts: Array.isArray(snapshot?.activeBolts) ? snapshot.activeBolts : []
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
if (normalizedFlow === 'simple') {
|
|
303
|
+
return {
|
|
304
|
+
activeSpecs: Array.isArray(snapshot?.activeSpecs) ? snapshot.activeSpecs : []
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
return {
|
|
308
|
+
activeRuns: Array.isArray(snapshot?.activeRuns) ? snapshot.activeRuns : []
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function getActivityCount(flow, activity) {
|
|
313
|
+
const normalizedFlow = normalizeFlowId(flow);
|
|
314
|
+
if (normalizedFlow === 'aidlc') {
|
|
315
|
+
return Array.isArray(activity?.activeBolts) ? activity.activeBolts.length : 0;
|
|
316
|
+
}
|
|
317
|
+
if (normalizedFlow === 'simple') {
|
|
318
|
+
return Array.isArray(activity?.activeSpecs) ? activity.activeSpecs.length : 0;
|
|
319
|
+
}
|
|
320
|
+
return Array.isArray(activity?.activeRuns) ? activity.activeRuns.length : 0;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function buildWorktreeEnvelope(flow, worktrees, selectedWorktreeId, cache, discovery) {
|
|
324
|
+
const normalizedFlow = normalizeFlowId(flow);
|
|
325
|
+
const items = (Array.isArray(worktrees) ? worktrees : []).map((worktree) => {
|
|
326
|
+
const availableFlows = detectAvailableFlows(worktree.path);
|
|
327
|
+
const flowAvailable = availableFlows.includes(normalizedFlow);
|
|
328
|
+
const cacheKey = `${normalizedFlow}:${worktree.id}`;
|
|
329
|
+
const cached = cache.get(cacheKey);
|
|
330
|
+
const status = flowAvailable
|
|
331
|
+
? (cached?.status || 'loading')
|
|
332
|
+
: 'unavailable';
|
|
333
|
+
const activity = cached?.activity || null;
|
|
334
|
+
const activeCount = getActivityCount(normalizedFlow, activity);
|
|
335
|
+
|
|
336
|
+
return {
|
|
337
|
+
...worktree,
|
|
338
|
+
isSelected: worktree.id === selectedWorktreeId,
|
|
339
|
+
flowAvailable,
|
|
340
|
+
status,
|
|
341
|
+
activeCount,
|
|
342
|
+
activity,
|
|
343
|
+
error: cached?.error || null
|
|
344
|
+
};
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
return {
|
|
348
|
+
flow: normalizedFlow,
|
|
349
|
+
source: discovery?.source || 'fallback',
|
|
350
|
+
isGitRepo: Boolean(discovery?.isGitRepo),
|
|
351
|
+
selectedWorktreeId,
|
|
352
|
+
hasPendingScans: items.some((item) => item.status === 'loading'),
|
|
353
|
+
error: discovery?.error,
|
|
354
|
+
items
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function getWatchRootsForEnvelope(flow, envelope) {
|
|
359
|
+
const normalizedFlow = normalizeFlowId(flow);
|
|
360
|
+
const config = FLOW_CONFIG[normalizedFlow];
|
|
361
|
+
if (!config) {
|
|
362
|
+
return [];
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const roots = [];
|
|
366
|
+
const items = Array.isArray(envelope?.items) ? envelope.items : [];
|
|
367
|
+
const selectedId = envelope?.selectedWorktreeId;
|
|
368
|
+
const selectedItem = items.find((item) => item.id === selectedId);
|
|
369
|
+
|
|
370
|
+
if (selectedItem) {
|
|
371
|
+
roots.push(resolveRootPathForFlow(selectedItem.path, normalizedFlow));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
for (const item of items) {
|
|
375
|
+
if (item.id === selectedId) {
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
if (!item.flowAvailable) {
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
if (item.activeCount <= 0 && item.status !== 'loading') {
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
roots.push(resolveRootPathForFlow(item.path, normalizedFlow));
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return Array.from(new Set(roots))
|
|
388
|
+
.filter((rootPath) => pathExistsAsDirectory(rootPath))
|
|
389
|
+
.slice(0, MAX_WORKTREE_WATCH_ROOTS);
|
|
390
|
+
}
|
|
391
|
+
|
|
77
392
|
function formatStaticFlowText(flow, snapshot, error) {
|
|
78
393
|
if (flow === 'fire') {
|
|
79
394
|
return formatDashboardText({
|
|
@@ -141,21 +456,160 @@ async function runFlowDashboard(options, flow, availableFlows = []) {
|
|
|
141
456
|
|
|
142
457
|
const watchEnabled = options.watch !== false;
|
|
143
458
|
const refreshMs = parseRefreshMs(options.refreshMs);
|
|
144
|
-
const
|
|
145
|
-
|
|
459
|
+
const explicitWorktreeSelection = typeof options.worktree === 'string' ? options.worktree.trim() : '';
|
|
460
|
+
let selectedWorktreeId = explicitWorktreeSelection || null;
|
|
461
|
+
let lastWorktreeEnvelope = null;
|
|
462
|
+
const activityCache = new Map();
|
|
463
|
+
const pendingProbes = new Map();
|
|
464
|
+
|
|
465
|
+
const parseSnapshotForFlow = async (flowId, context = {}) => {
|
|
466
|
+
const normalizedFlow = normalizeFlowId(flowId);
|
|
467
|
+
const flowConfig = FLOW_CONFIG[normalizedFlow];
|
|
146
468
|
if (!flowConfig) {
|
|
147
469
|
return {
|
|
148
470
|
ok: false,
|
|
149
471
|
error: {
|
|
150
472
|
code: 'UNSUPPORTED_FLOW',
|
|
151
|
-
message: `Flow \"${
|
|
473
|
+
message: `Flow \"${normalizedFlow}\" is not supported.`
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const discovery = discoverGitWorktrees(workspacePath);
|
|
479
|
+
const worktrees = Array.isArray(discovery?.worktrees) ? discovery.worktrees : [];
|
|
480
|
+
const requestedWorktreeSelector = String(context.selectedWorktreeId || selectedWorktreeId || explicitWorktreeSelection || '').trim();
|
|
481
|
+
const selectedWorktree = pickWorktree(worktrees, requestedWorktreeSelector, workspacePath)
|
|
482
|
+
|| pickWorktree(worktrees, workspacePath, workspacePath)
|
|
483
|
+
|| worktrees[0];
|
|
484
|
+
|
|
485
|
+
selectedWorktreeId = selectedWorktree?.id || selectedWorktreeId;
|
|
486
|
+
|
|
487
|
+
const selectedResult = selectedWorktree
|
|
488
|
+
? await flowConfig.parse(selectedWorktree.path)
|
|
489
|
+
: {
|
|
490
|
+
ok: false,
|
|
491
|
+
error: {
|
|
492
|
+
code: 'WORKTREE_NOT_FOUND',
|
|
493
|
+
message: 'No selectable worktree was found for dashboard parsing.'
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
if (selectedWorktree) {
|
|
498
|
+
const cacheKey = `${normalizedFlow}:${selectedWorktree.id}`;
|
|
499
|
+
if (selectedResult?.ok) {
|
|
500
|
+
activityCache.set(cacheKey, {
|
|
501
|
+
status: 'ready',
|
|
502
|
+
activity: extractActivityFromSnapshot(normalizedFlow, selectedResult.snapshot),
|
|
503
|
+
error: null,
|
|
504
|
+
updatedAt: Date.now()
|
|
505
|
+
});
|
|
506
|
+
} else {
|
|
507
|
+
activityCache.set(cacheKey, {
|
|
508
|
+
status: 'error',
|
|
509
|
+
activity: null,
|
|
510
|
+
error: selectedResult?.error || null,
|
|
511
|
+
updatedAt: Date.now()
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
for (const worktree of worktrees) {
|
|
517
|
+
if (!worktree || worktree.id === selectedWorktreeId) {
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const availableFlowsForWorktree = detectAvailableFlows(worktree.path);
|
|
522
|
+
const flowAvailable = availableFlowsForWorktree.includes(normalizedFlow);
|
|
523
|
+
const cacheKey = `${normalizedFlow}:${worktree.id}`;
|
|
524
|
+
|
|
525
|
+
if (!flowAvailable) {
|
|
526
|
+
activityCache.set(cacheKey, {
|
|
527
|
+
status: 'unavailable',
|
|
528
|
+
activity: null,
|
|
529
|
+
error: null,
|
|
530
|
+
updatedAt: Date.now()
|
|
531
|
+
});
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const cached = activityCache.get(cacheKey);
|
|
536
|
+
const shouldRefreshProbe = !cached || (Date.now() - (cached.updatedAt || 0)) > 2000;
|
|
537
|
+
if (!shouldRefreshProbe || pendingProbes.has(cacheKey)) {
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
activityCache.set(cacheKey, {
|
|
542
|
+
status: 'loading',
|
|
543
|
+
activity: cached?.activity || null,
|
|
544
|
+
error: null,
|
|
545
|
+
updatedAt: Date.now()
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
const probePromise = Promise.resolve()
|
|
549
|
+
.then(() => probeWorktreeActivity(normalizedFlow, worktree.path))
|
|
550
|
+
.then((activity) => {
|
|
551
|
+
activityCache.set(cacheKey, {
|
|
552
|
+
status: 'ready',
|
|
553
|
+
activity,
|
|
554
|
+
error: null,
|
|
555
|
+
updatedAt: Date.now()
|
|
556
|
+
});
|
|
557
|
+
})
|
|
558
|
+
.catch((probeError) => {
|
|
559
|
+
activityCache.set(cacheKey, {
|
|
560
|
+
status: 'error',
|
|
561
|
+
activity: null,
|
|
562
|
+
error: {
|
|
563
|
+
code: 'WORKTREE_PROBE_ERROR',
|
|
564
|
+
message: probeError?.message || String(probeError)
|
|
565
|
+
},
|
|
566
|
+
updatedAt: Date.now()
|
|
567
|
+
});
|
|
568
|
+
})
|
|
569
|
+
.finally(() => {
|
|
570
|
+
pendingProbes.delete(cacheKey);
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
pendingProbes.set(cacheKey, probePromise);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const envelope = buildWorktreeEnvelope(
|
|
577
|
+
normalizedFlow,
|
|
578
|
+
worktrees,
|
|
579
|
+
selectedWorktreeId,
|
|
580
|
+
activityCache,
|
|
581
|
+
discovery
|
|
582
|
+
);
|
|
583
|
+
lastWorktreeEnvelope = envelope;
|
|
584
|
+
|
|
585
|
+
if (!selectedResult?.ok) {
|
|
586
|
+
return {
|
|
587
|
+
ok: false,
|
|
588
|
+
error: {
|
|
589
|
+
...(selectedResult?.error || {
|
|
590
|
+
code: 'PARSE_ERROR',
|
|
591
|
+
message: 'Unable to parse selected worktree snapshot.'
|
|
592
|
+
}),
|
|
593
|
+
details: selectedWorktree
|
|
594
|
+
? `worktree: ${selectedWorktree.displayBranch} (${selectedWorktree.path})`
|
|
595
|
+
: undefined
|
|
152
596
|
}
|
|
153
597
|
};
|
|
154
598
|
}
|
|
155
|
-
|
|
599
|
+
|
|
600
|
+
return {
|
|
601
|
+
ok: true,
|
|
602
|
+
snapshot: {
|
|
603
|
+
...selectedResult.snapshot,
|
|
604
|
+
workspacePath: selectedWorktree?.path || selectedResult.snapshot?.workspacePath || workspacePath,
|
|
605
|
+
dashboardWorktrees: envelope
|
|
606
|
+
}
|
|
607
|
+
};
|
|
156
608
|
};
|
|
157
609
|
|
|
158
|
-
const initialResult = await parseSnapshotForFlow(flow
|
|
610
|
+
const initialResult = await parseSnapshotForFlow(flow, {
|
|
611
|
+
selectedWorktreeId: selectedWorktreeId || explicitWorktreeSelection || workspacePath
|
|
612
|
+
});
|
|
159
613
|
clearTerminalOutput();
|
|
160
614
|
|
|
161
615
|
if (!watchEnabled) {
|
|
@@ -190,6 +644,16 @@ async function runFlowDashboard(options, flow, availableFlows = []) {
|
|
|
190
644
|
flow,
|
|
191
645
|
availableFlows: flowIds,
|
|
192
646
|
resolveRootPathForFlow: (flowId) => resolveRootPathForFlow(workspacePath, flowId),
|
|
647
|
+
resolveRootPathsForFlow: (flowId) => {
|
|
648
|
+
if (!lastWorktreeEnvelope) {
|
|
649
|
+
return [resolveRootPathForFlow(workspacePath, flowId)];
|
|
650
|
+
}
|
|
651
|
+
const roots = getWatchRootsForEnvelope(flowId, lastWorktreeEnvelope);
|
|
652
|
+
if (roots.length > 0) {
|
|
653
|
+
return roots;
|
|
654
|
+
}
|
|
655
|
+
return [resolveRootPathForFlow(workspacePath, flowId)];
|
|
656
|
+
},
|
|
193
657
|
refreshMs,
|
|
194
658
|
watchEnabled,
|
|
195
659
|
initialSnapshot: initialResult.ok ? initialResult.snapshot : null,
|
|
@@ -236,5 +700,7 @@ module.exports = {
|
|
|
236
700
|
parseRefreshMs,
|
|
237
701
|
formatStaticFlowText,
|
|
238
702
|
clearTerminalOutput,
|
|
239
|
-
createInkStdout
|
|
703
|
+
createInkStdout,
|
|
704
|
+
probeWorktreeActivity,
|
|
705
|
+
getWatchRootsForEnvelope
|
|
240
706
|
};
|
|
@@ -32,13 +32,19 @@ function createDebouncedTrigger(callback, delayMs) {
|
|
|
32
32
|
function createWatchRuntime(options) {
|
|
33
33
|
const {
|
|
34
34
|
rootPath,
|
|
35
|
+
rootPaths,
|
|
35
36
|
onRefresh,
|
|
36
37
|
onError,
|
|
37
38
|
debounceMs = 250
|
|
38
39
|
} = options;
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
|
|
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');
|
|
42
48
|
}
|
|
43
49
|
|
|
44
50
|
if (typeof onRefresh !== 'function') {
|
|
@@ -51,12 +57,14 @@ function createWatchRuntime(options) {
|
|
|
51
57
|
let started = false;
|
|
52
58
|
const debounced = createDebouncedTrigger(onRefresh, debounceMs);
|
|
53
59
|
|
|
54
|
-
const watchTargets = [
|
|
55
|
-
path.join(
|
|
56
|
-
path.join(
|
|
57
|
-
path.join(
|
|
58
|
-
path.join(
|
|
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
|
+
]));
|
|
60
68
|
|
|
61
69
|
function start() {
|
|
62
70
|
if (started) {
|
|
@@ -103,7 +111,8 @@ function createWatchRuntime(options) {
|
|
|
103
111
|
start,
|
|
104
112
|
close,
|
|
105
113
|
isActive: () => started,
|
|
106
|
-
hasPendingRefresh: () => debounced.isPending()
|
|
114
|
+
hasPendingRefresh: () => debounced.isPending(),
|
|
115
|
+
getRoots: () => [...roots]
|
|
107
116
|
};
|
|
108
117
|
}
|
|
109
118
|
|