@exaudeus/workrail 3.14.0 → 3.16.0
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/dist/application/services/validation-engine.js +4 -9
- package/dist/application/services/workflow-compiler.js +4 -6
- package/dist/application/services/workflow-service.d.ts +2 -0
- package/dist/application/services/workflow-service.js +3 -0
- package/dist/console/assets/index-BE5PAgPO.js +28 -0
- package/dist/console/assets/index-BZNM03t1.css +1 -0
- package/dist/console/index.html +2 -2
- package/dist/engine/engine-factory.js +2 -2
- package/dist/engine/types.d.ts +1 -1
- package/dist/env-flags.d.ts +1 -0
- package/dist/env-flags.js +4 -0
- package/dist/infrastructure/session/HttpServer.d.ts +3 -3
- package/dist/infrastructure/session/HttpServer.js +68 -74
- package/dist/infrastructure/storage/caching-workflow-storage.d.ts +2 -0
- package/dist/infrastructure/storage/caching-workflow-storage.js +15 -6
- package/dist/infrastructure/storage/file-workflow-storage.js +3 -4
- package/dist/infrastructure/storage/schema-validating-workflow-storage.js +9 -8
- package/dist/manifest.json +283 -219
- package/dist/mcp/assert-output.d.ts +37 -0
- package/dist/mcp/assert-output.js +52 -0
- package/dist/mcp/boundary-coercion.d.ts +1 -0
- package/dist/mcp/boundary-coercion.js +44 -0
- package/dist/mcp/dev-mode.d.ts +1 -0
- package/dist/mcp/dev-mode.js +4 -0
- package/dist/mcp/handler-factory.js +12 -9
- package/dist/mcp/handlers/session.js +8 -9
- package/dist/mcp/handlers/shared/request-workflow-reader.d.ts +5 -0
- package/dist/mcp/handlers/shared/request-workflow-reader.js +47 -2
- package/dist/mcp/handlers/v2-advance-core/assessment-consequences.d.ts +1 -1
- package/dist/mcp/handlers/v2-advance-core/assessment-consequences.js +4 -5
- package/dist/mcp/handlers/v2-advance-core/event-builders.d.ts +2 -0
- package/dist/mcp/handlers/v2-advance-core/event-builders.js +6 -6
- package/dist/mcp/handlers/v2-advance-core/index.d.ts +2 -0
- package/dist/mcp/handlers/v2-advance-core/index.js +5 -4
- package/dist/mcp/handlers/v2-advance-core/input-validation.d.ts +2 -0
- package/dist/mcp/handlers/v2-advance-core/input-validation.js +32 -9
- package/dist/mcp/handlers/v2-advance-core/outcome-blocked.d.ts +2 -0
- package/dist/mcp/handlers/v2-advance-core/outcome-blocked.js +2 -2
- package/dist/mcp/handlers/v2-advance-core/outcome-success.d.ts +2 -0
- package/dist/mcp/handlers/v2-advance-core/outcome-success.js +1 -1
- package/dist/mcp/handlers/v2-checkpoint.d.ts +1 -1
- package/dist/mcp/handlers/v2-checkpoint.js +5 -6
- package/dist/mcp/handlers/v2-execution/advance.d.ts +4 -2
- package/dist/mcp/handlers/v2-execution/advance.js +5 -7
- package/dist/mcp/handlers/v2-execution/continue-advance.js +56 -26
- package/dist/mcp/handlers/v2-execution/continue-rehydrate.d.ts +1 -1
- package/dist/mcp/handlers/v2-execution/continue-rehydrate.js +9 -9
- package/dist/mcp/handlers/v2-execution/replay.d.ts +6 -4
- package/dist/mcp/handlers/v2-execution/replay.js +47 -30
- package/dist/mcp/handlers/v2-execution/start.d.ts +3 -3
- package/dist/mcp/handlers/v2-execution/start.js +31 -12
- package/dist/mcp/handlers/v2-execution/workflow-object-cache.d.ts +5 -0
- package/dist/mcp/handlers/v2-execution/workflow-object-cache.js +19 -0
- package/dist/mcp/handlers/v2-execution-helpers.d.ts +1 -0
- package/dist/mcp/handlers/v2-execution-helpers.js +23 -7
- package/dist/mcp/handlers/v2-resume.d.ts +1 -1
- package/dist/mcp/handlers/v2-resume.js +3 -4
- package/dist/mcp/handlers/v2-state-conversion.js +5 -1
- package/dist/mcp/handlers/v2-workflow.d.ts +100 -0
- package/dist/mcp/handlers/v2-workflow.js +155 -31
- package/dist/mcp/handlers/workflow.d.ts +2 -5
- package/dist/mcp/handlers/workflow.js +15 -12
- package/dist/mcp/output-schemas.d.ts +123 -29
- package/dist/mcp/output-schemas.js +36 -18
- package/dist/mcp/server.js +70 -5
- package/dist/mcp/tool-call-timing.d.ts +24 -0
- package/dist/mcp/tool-call-timing.js +85 -0
- package/dist/mcp/tool-descriptions.js +17 -9
- package/dist/mcp/transports/http-entry.js +3 -2
- package/dist/mcp/transports/http-listener.d.ts +1 -0
- package/dist/mcp/transports/http-listener.js +25 -0
- package/dist/mcp/transports/shutdown-hooks.d.ts +4 -1
- package/dist/mcp/transports/shutdown-hooks.js +3 -2
- package/dist/mcp/transports/stdio-entry.js +6 -28
- package/dist/mcp/v2/tools.d.ts +6 -0
- package/dist/mcp/v2/tools.js +2 -0
- package/dist/mcp/v2-response-formatter.js +2 -4
- package/dist/mcp/validation/schema-introspection.d.ts +1 -0
- package/dist/mcp/validation/schema-introspection.js +15 -5
- package/dist/mcp/validation/suggestion-generator.js +2 -2
- package/dist/mcp/workflow-protocol-contracts.js +5 -1
- package/dist/runtime/adapters/node-process-signals.d.ts +1 -0
- package/dist/runtime/adapters/node-process-signals.js +5 -0
- package/dist/runtime/adapters/noop-process-signals.d.ts +1 -0
- package/dist/runtime/adapters/noop-process-signals.js +2 -0
- package/dist/runtime/ports/process-signals.d.ts +1 -0
- package/dist/types/workflow-definition.d.ts +3 -2
- package/dist/types/workflow.d.ts +3 -0
- package/dist/types/workflow.js +35 -26
- package/dist/v2/durable-core/domain/context-template-resolver.js +2 -2
- package/dist/v2/durable-core/domain/function-definition-expander.js +2 -17
- package/dist/v2/durable-core/domain/prompt-renderer.d.ts +1 -0
- package/dist/v2/durable-core/domain/prompt-renderer.js +23 -18
- package/dist/v2/durable-core/domain/recap-recovery.js +23 -16
- package/dist/v2/durable-core/domain/retrieval-contract.js +13 -7
- package/dist/v2/durable-core/session-index.d.ts +22 -0
- package/dist/v2/durable-core/session-index.js +58 -0
- package/dist/v2/durable-core/sorted-event-log.d.ts +6 -0
- package/dist/v2/durable-core/sorted-event-log.js +15 -0
- package/dist/v2/infra/local/fs/index.js +8 -8
- package/dist/v2/infra/local/session-store/index.d.ts +1 -1
- package/dist/v2/infra/local/session-store/index.js +71 -61
- package/dist/v2/infra/local/session-summary-provider/index.js +9 -4
- package/dist/v2/infra/local/snapshot-store/index.js +2 -1
- package/dist/v2/infra/local/workspace-anchor/index.js +4 -1
- package/dist/v2/ports/session-event-log-store.port.d.ts +1 -1
- package/dist/v2/projections/assessment-consequences.d.ts +2 -1
- package/dist/v2/projections/assessment-consequences.js +0 -5
- package/dist/v2/projections/assessments.d.ts +2 -1
- package/dist/v2/projections/assessments.js +2 -4
- package/dist/v2/projections/gaps.d.ts +2 -1
- package/dist/v2/projections/gaps.js +0 -5
- package/dist/v2/projections/preferences.d.ts +2 -1
- package/dist/v2/projections/preferences.js +0 -5
- package/dist/v2/projections/run-context.d.ts +2 -2
- package/dist/v2/projections/run-context.js +0 -5
- package/dist/v2/projections/run-dag.js +7 -1
- package/dist/v2/projections/run-execution-trace.d.ts +8 -0
- package/dist/v2/projections/run-execution-trace.js +124 -0
- package/dist/v2/projections/run-status-signals.d.ts +2 -2
- package/dist/v2/usecases/console-routes.d.ts +3 -1
- package/dist/v2/usecases/console-routes.js +149 -3
- package/dist/v2/usecases/console-service.d.ts +2 -0
- package/dist/v2/usecases/console-service.js +87 -26
- package/dist/v2/usecases/console-types.d.ts +65 -0
- package/dist/v2/usecases/worktree-service.js +87 -8
- package/package.json +7 -6
- package/spec/authoring-spec.json +82 -1
- package/spec/workflow-tags.json +132 -0
- package/spec/workflow.schema.json +21 -11
- package/workflows/adaptive-ticket-creation.json +33 -8
- package/workflows/architecture-scalability-audit.json +50 -9
- package/workflows/bug-investigation.agentic.v2.json +43 -14
- package/workflows/coding-task-workflow-agentic.json +57 -38
- package/workflows/coding-task-workflow-agentic.lean.v2.json +129 -34
- package/workflows/coding-task-workflow-agentic.v2.json +97 -30
- package/workflows/cross-platform-code-conversion.v2.json +175 -48
- package/workflows/document-creation-workflow.json +49 -12
- package/workflows/documentation-update-workflow.json +9 -2
- package/workflows/intelligent-test-case-generation.json +9 -2
- package/workflows/learner-centered-course-workflow.json +273 -266
- package/workflows/mr-review-workflow.agentic.v2.json +88 -14
- package/workflows/personal-learning-materials-creation-branched.json +181 -174
- package/workflows/presentation-creation.json +167 -160
- package/workflows/production-readiness-audit.json +61 -15
- package/workflows/relocation-workflow-us.json +21 -5
- package/workflows/routines/tension-driven-design.json +1 -1
- package/workflows/scoped-documentation-workflow.json +9 -2
- package/workflows/test-artifact-loop-control.json +1 -2
- package/workflows/ui-ux-design-workflow.json +334 -0
- package/workflows/workflow-diagnose-environment.json +7 -1
- package/workflows/workflow-for-workflows.json +514 -484
- package/workflows/workflow-for-workflows.v2.json +55 -11
- package/workflows/wr.discovery.json +118 -29
- package/dist/console/assets/index-DW78t31j.css +0 -1
- package/dist/console/assets/index-EsSXrC_a.js +0 -28
|
@@ -8,6 +8,25 @@ const express_1 = __importDefault(require("express"));
|
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const fs_1 = __importDefault(require("fs"));
|
|
10
10
|
const worktree_service_js_1 = require("./worktree-service.js");
|
|
11
|
+
const dev_mode_js_1 = require("../../mcp/dev-mode.js");
|
|
12
|
+
function watchSessionsDir(sessionsDir, onChanged) {
|
|
13
|
+
try {
|
|
14
|
+
fs_1.default.mkdirSync(sessionsDir, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
catch { }
|
|
17
|
+
let watcher = null;
|
|
18
|
+
try {
|
|
19
|
+
watcher = fs_1.default.watch(sessionsDir, { recursive: true }, (_eventType, filename) => {
|
|
20
|
+
if (filename !== null && filename.endsWith('.jsonl')) {
|
|
21
|
+
onChanged();
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
watcher.on('error', () => { });
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
}
|
|
28
|
+
return () => { watcher?.close(); };
|
|
29
|
+
}
|
|
11
30
|
function resolveConsoleDist() {
|
|
12
31
|
const releasedDist = path_1.default.join(__dirname, '../../console');
|
|
13
32
|
if (fs_1.default.existsSync(releasedDist))
|
|
@@ -20,13 +39,65 @@ function resolveConsoleDist() {
|
|
|
20
39
|
return legacyConsoleDist;
|
|
21
40
|
return null;
|
|
22
41
|
}
|
|
23
|
-
|
|
42
|
+
let cachedWorkflowTags = null;
|
|
43
|
+
function loadWorkflowTags() {
|
|
44
|
+
if (cachedWorkflowTags !== null)
|
|
45
|
+
return cachedWorkflowTags;
|
|
46
|
+
const tagsPath = path_1.default.resolve(__dirname, '../../../spec/workflow-tags.json');
|
|
47
|
+
try {
|
|
48
|
+
cachedWorkflowTags = JSON.parse(fs_1.default.readFileSync(tagsPath, 'utf8'));
|
|
49
|
+
return cachedWorkflowTags;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return { version: 0, tags: [], workflows: {} };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function mountConsoleRoutes(app, consoleService, workflowService, timingRingBuffer) {
|
|
56
|
+
const sseClients = new Set();
|
|
57
|
+
let sseDebounceTimer = null;
|
|
58
|
+
function broadcastChange() {
|
|
59
|
+
if (sseDebounceTimer !== null)
|
|
60
|
+
return;
|
|
61
|
+
sseDebounceTimer = setTimeout(() => {
|
|
62
|
+
sseDebounceTimer = null;
|
|
63
|
+
for (const client of sseClients) {
|
|
64
|
+
try {
|
|
65
|
+
client.write('data: {"type":"change"}\n\n');
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
sseClients.delete(client);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}, 200);
|
|
72
|
+
}
|
|
73
|
+
const stopWatcher = watchSessionsDir(consoleService.getSessionsDir(), broadcastChange);
|
|
74
|
+
app.get('/api/v2/workspace/events', (req, res) => {
|
|
75
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
76
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
77
|
+
res.setHeader('Connection', 'keep-alive');
|
|
78
|
+
res.setHeader('X-Accel-Buffering', 'no');
|
|
79
|
+
res.flushHeaders();
|
|
80
|
+
res.write('data: {"type":"connected"}\n\n');
|
|
81
|
+
sseClients.add(res);
|
|
82
|
+
req.on('close', () => { sseClients.delete(res); });
|
|
83
|
+
res.on('close', () => { sseClients.delete(res); });
|
|
84
|
+
});
|
|
85
|
+
if (dev_mode_js_1.DEV_MODE) {
|
|
86
|
+
app.get('/api/v2/perf/tool-calls', (req, res) => {
|
|
87
|
+
const rawLimit = req.query['limit'];
|
|
88
|
+
const limit = typeof rawLimit === 'string' ? parseInt(rawLimit, 10) : undefined;
|
|
89
|
+
const safeLimit = (limit !== undefined && Number.isFinite(limit) && limit > 0) ? limit : undefined;
|
|
90
|
+
const observations = timingRingBuffer ? timingRingBuffer.recent(safeLimit) : [];
|
|
91
|
+
res.json({ success: true, data: { observations, total: timingRingBuffer?.size ?? 0, devMode: dev_mode_js_1.DEV_MODE } });
|
|
92
|
+
});
|
|
93
|
+
}
|
|
24
94
|
app.get('/api/v2/sessions', async (_req, res) => {
|
|
25
95
|
const result = await consoleService.getSessionList();
|
|
26
96
|
result.match((data) => res.json({ success: true, data }), (error) => res.status(500).json({ success: false, error: error.message }));
|
|
27
97
|
});
|
|
28
98
|
let cwdRepoRootPromise = null;
|
|
29
99
|
const REPO_ROOTS_TTL_MS = 60000;
|
|
100
|
+
const REPO_ROOT_SESSION_STALENESS_MS = 30 * 24 * 60 * 60 * 1000;
|
|
30
101
|
let cachedRepoRoots = [];
|
|
31
102
|
let repoRootsExpiresAt = 0;
|
|
32
103
|
app.get('/api/v2/worktrees', async (_req, res) => {
|
|
@@ -37,7 +108,13 @@ function mountConsoleRoutes(app, consoleService) {
|
|
|
37
108
|
if (Date.now() > repoRootsExpiresAt) {
|
|
38
109
|
cwdRepoRootPromise ?? (cwdRepoRootPromise = (0, worktree_service_js_1.resolveRepoRoot)(process.cwd()));
|
|
39
110
|
const cwdRoot = await cwdRepoRootPromise;
|
|
40
|
-
const
|
|
111
|
+
const cutoffMs = Date.now() - REPO_ROOT_SESSION_STALENESS_MS;
|
|
112
|
+
const rawRoots = sessions
|
|
113
|
+
.filter(s => s.lastModifiedMs >= cutoffMs)
|
|
114
|
+
.map(s => s.repoRoot)
|
|
115
|
+
.filter((r) => r !== null);
|
|
116
|
+
const resolvedRoots = await Promise.all(rawRoots.map(r => (0, worktree_service_js_1.resolveRepoRoot)(r)));
|
|
117
|
+
const repoRootSet = new Set(resolvedRoots.filter((r) => r !== null));
|
|
41
118
|
if (cwdRoot !== null)
|
|
42
119
|
repoRootSet.add(cwdRoot);
|
|
43
120
|
cachedRepoRoots = [...repoRootSet];
|
|
@@ -68,10 +145,78 @@ function mountConsoleRoutes(app, consoleService) {
|
|
|
68
145
|
res.status(status).json({ success: false, error: error.message });
|
|
69
146
|
});
|
|
70
147
|
});
|
|
148
|
+
if (workflowService) {
|
|
149
|
+
app.get('/api/v2/workflows', async (_req, res) => {
|
|
150
|
+
try {
|
|
151
|
+
const tagsFile = loadWorkflowTags();
|
|
152
|
+
const allWorkflows = await workflowService.loadAllWorkflows();
|
|
153
|
+
const workflows = allWorkflows
|
|
154
|
+
.filter((w) => !tagsFile.workflows[w.definition.id]?.hidden)
|
|
155
|
+
.map((w) => {
|
|
156
|
+
const { definition, source } = w;
|
|
157
|
+
const tagEntry = tagsFile.workflows[definition.id];
|
|
158
|
+
return {
|
|
159
|
+
id: definition.id,
|
|
160
|
+
name: definition.name,
|
|
161
|
+
description: definition.description,
|
|
162
|
+
version: definition.version,
|
|
163
|
+
tags: tagEntry?.tags ?? [],
|
|
164
|
+
source,
|
|
165
|
+
...(definition.about !== undefined ? { about: definition.about } : {}),
|
|
166
|
+
...(definition.examples?.length ? { examples: [...definition.examples] } : {}),
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
res.json({ success: true, data: { workflows } });
|
|
170
|
+
}
|
|
171
|
+
catch (e) {
|
|
172
|
+
res.status(500).json({ success: false, error: e instanceof Error ? e.message : String(e) });
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
app.get('/api/v2/workflows/:workflowId', async (req, res) => {
|
|
176
|
+
const { workflowId } = req.params;
|
|
177
|
+
try {
|
|
178
|
+
const workflow = await workflowService.getWorkflowById(workflowId);
|
|
179
|
+
if (!workflow) {
|
|
180
|
+
return res.status(404).json({ success: false, error: `Workflow not found: ${workflowId}` });
|
|
181
|
+
}
|
|
182
|
+
const tagsFile = loadWorkflowTags();
|
|
183
|
+
if (tagsFile.workflows[workflowId]?.hidden) {
|
|
184
|
+
return res.status(404).json({ success: false, error: `Workflow not found: ${workflowId}` });
|
|
185
|
+
}
|
|
186
|
+
const { definition, source } = workflow;
|
|
187
|
+
const tagEntry = tagsFile.workflows[workflowId];
|
|
188
|
+
return res.json({
|
|
189
|
+
success: true,
|
|
190
|
+
data: {
|
|
191
|
+
id: definition.id,
|
|
192
|
+
name: definition.name,
|
|
193
|
+
description: definition.description,
|
|
194
|
+
version: definition.version,
|
|
195
|
+
tags: tagEntry?.tags ?? [],
|
|
196
|
+
source,
|
|
197
|
+
stepCount: definition.steps.length,
|
|
198
|
+
...(definition.about !== undefined ? { about: definition.about } : {}),
|
|
199
|
+
...(definition.examples?.length ? { examples: [...definition.examples] } : {}),
|
|
200
|
+
...(definition.preconditions?.length ? { preconditions: [...definition.preconditions] } : {}),
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
catch (e) {
|
|
205
|
+
return res.status(500).json({ success: false, error: e instanceof Error ? e.message : String(e) });
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
}
|
|
71
209
|
const consoleDist = resolveConsoleDist();
|
|
72
210
|
if (consoleDist) {
|
|
73
|
-
app.use('/console', express_1.default.static(consoleDist
|
|
211
|
+
app.use('/console', express_1.default.static(consoleDist, {
|
|
212
|
+
setHeaders(res, filePath) {
|
|
213
|
+
if (path_1.default.basename(filePath) === 'index.html') {
|
|
214
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}));
|
|
74
218
|
app.get('/console/*path', (_req, res) => {
|
|
219
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
75
220
|
res.sendFile(path_1.default.join(consoleDist, 'index.html'));
|
|
76
221
|
});
|
|
77
222
|
console.error(`[Console] UI serving from ${consoleDist}`);
|
|
@@ -85,4 +230,5 @@ function mountConsoleRoutes(app, consoleService) {
|
|
|
85
230
|
});
|
|
86
231
|
console.error('[Console] UI not found (run: cd console && npm run build)');
|
|
87
232
|
}
|
|
233
|
+
return stopWatcher;
|
|
88
234
|
}
|
|
@@ -15,6 +15,8 @@ export interface ConsoleServicePorts {
|
|
|
15
15
|
export declare class ConsoleService {
|
|
16
16
|
private readonly ports;
|
|
17
17
|
constructor(ports: ConsoleServicePorts);
|
|
18
|
+
private readonly _summaryCache;
|
|
19
|
+
getSessionsDir(): string;
|
|
18
20
|
getSessionList(): ResultAsync<ConsoleSessionListResponse, ConsoleServiceError>;
|
|
19
21
|
getSessionDetail(sessionIdStr: string): ResultAsync<ConsoleSessionDetail, ConsoleServiceError>;
|
|
20
22
|
getNodeDetail(sessionIdStr: string, nodeId: string): ResultAsync<ConsoleNodeDetail, ConsoleServiceError>;
|
|
@@ -11,6 +11,8 @@ const node_outputs_js_1 = require("../projections/node-outputs.js");
|
|
|
11
11
|
const advance_outcomes_js_1 = require("../projections/advance-outcomes.js");
|
|
12
12
|
const artifacts_js_1 = require("../projections/artifacts.js");
|
|
13
13
|
const run_context_js_1 = require("../projections/run-context.js");
|
|
14
|
+
const sorted_event_log_js_1 = require("../durable-core/sorted-event-log.js");
|
|
15
|
+
const run_execution_trace_js_1 = require("../projections/run-execution-trace.js");
|
|
14
16
|
const constants_js_1 = require("../durable-core/constants.js");
|
|
15
17
|
const index_js_1 = require("../durable-core/ids/index.js");
|
|
16
18
|
const MAX_SESSIONS_TO_LOAD = 500;
|
|
@@ -18,6 +20,10 @@ const DORMANCY_THRESHOLD_MS = 3 * 24 * 60 * 60 * 1000;
|
|
|
18
20
|
class ConsoleService {
|
|
19
21
|
constructor(ports) {
|
|
20
22
|
this.ports = ports;
|
|
23
|
+
this._summaryCache = new Map();
|
|
24
|
+
}
|
|
25
|
+
getSessionsDir() {
|
|
26
|
+
return this.ports.dataDir.sessionsDir();
|
|
21
27
|
}
|
|
22
28
|
getSessionList() {
|
|
23
29
|
return this.ports.directoryListing
|
|
@@ -100,6 +106,10 @@ class ConsoleService {
|
|
|
100
106
|
});
|
|
101
107
|
}
|
|
102
108
|
loadSessionSummary(sessionId, lastModifiedMs, nowMs) {
|
|
109
|
+
const cached = this._summaryCache.get(sessionId);
|
|
110
|
+
if (cached !== undefined && cached.mtime === lastModifiedMs) {
|
|
111
|
+
return (0, neverthrow_2.okAsync)(cached.summary);
|
|
112
|
+
}
|
|
103
113
|
return this.ports.sessionStore
|
|
104
114
|
.load(sessionId)
|
|
105
115
|
.andThen((truth) => {
|
|
@@ -107,10 +117,22 @@ class ConsoleService {
|
|
|
107
117
|
const workflowNamesRA = dagRes.isOk()
|
|
108
118
|
? resolveWorkflowNames(dagRes.value, this.ports.pinnedWorkflowStore)
|
|
109
119
|
: (0, neverthrow_2.okAsync)({});
|
|
120
|
+
const completionRA = dagRes.isOk()
|
|
121
|
+
? resolveRunCompletionFromDag(dagRes.value, this.ports.snapshotStore)
|
|
122
|
+
: (0, neverthrow_2.okAsync)({});
|
|
110
123
|
return neverthrow_1.ResultAsync.combine([
|
|
111
|
-
|
|
124
|
+
completionRA,
|
|
112
125
|
workflowNamesRA,
|
|
113
|
-
]).map(([completionMap, workflowNames]) =>
|
|
126
|
+
]).map(([completionMap, workflowNames]) => {
|
|
127
|
+
const dag = dagRes.isOk() ? dagRes.value : undefined;
|
|
128
|
+
return projectSessionSummary(sessionId, truth, completionMap, workflowNames, lastModifiedMs, nowMs, dag);
|
|
129
|
+
});
|
|
130
|
+
})
|
|
131
|
+
.map((summary) => {
|
|
132
|
+
if (summary !== null && summary.status !== 'in_progress') {
|
|
133
|
+
this._summaryCache.set(sessionId, { mtime: lastModifiedMs, summary });
|
|
134
|
+
}
|
|
135
|
+
return summary;
|
|
114
136
|
})
|
|
115
137
|
.orElse(() => (0, neverthrow_2.okAsync)(null));
|
|
116
138
|
}
|
|
@@ -236,19 +258,19 @@ function extractStepTitlesFromCompiled(compiled) {
|
|
|
236
258
|
return titles;
|
|
237
259
|
}
|
|
238
260
|
const TITLE_CONTEXT_KEYS = ['goal', 'taskDescription', 'mrTitle', 'prTitle', 'ticketTitle', 'problem'];
|
|
239
|
-
function deriveSessionTitle(
|
|
240
|
-
const contextRes = (0, run_context_js_1.projectRunContextV2)(
|
|
261
|
+
function deriveSessionTitle(sortedEvents) {
|
|
262
|
+
const contextRes = (0, run_context_js_1.projectRunContextV2)(sortedEvents);
|
|
241
263
|
if (contextRes.isOk()) {
|
|
242
264
|
for (const runCtx of Object.values(contextRes.value.byRunId)) {
|
|
243
265
|
for (const key of TITLE_CONTEXT_KEYS) {
|
|
244
266
|
const val = runCtx.context[key];
|
|
245
267
|
if (typeof val === 'string' && val.trim().length > 0) {
|
|
246
|
-
return
|
|
268
|
+
return val.trim();
|
|
247
269
|
}
|
|
248
270
|
}
|
|
249
271
|
}
|
|
250
272
|
}
|
|
251
|
-
const title = extractTitleFromFirstRecap(
|
|
273
|
+
const title = extractTitleFromFirstRecap(sortedEvents);
|
|
252
274
|
if (title)
|
|
253
275
|
return title;
|
|
254
276
|
return null;
|
|
@@ -330,19 +352,26 @@ function truncateTitle(text, maxLen = 120) {
|
|
|
330
352
|
return text;
|
|
331
353
|
return text.slice(0, maxLen - 1) + '…';
|
|
332
354
|
}
|
|
333
|
-
function projectSessionSummary(sessionId, truth, completionByRunId, workflowNames, lastModifiedMs, nowMs) {
|
|
355
|
+
function projectSessionSummary(sessionId, truth, completionByRunId, workflowNames, lastModifiedMs, nowMs, precomputedDag) {
|
|
334
356
|
const { events } = truth;
|
|
335
357
|
const health = (0, session_health_js_1.projectSessionHealthV2)(truth);
|
|
336
358
|
if (health.isErr())
|
|
337
359
|
return null;
|
|
338
360
|
const sessionHealth = health.value.kind === 'healthy' ? 'healthy' : 'corrupt';
|
|
339
|
-
|
|
340
|
-
if (
|
|
361
|
+
let dag;
|
|
362
|
+
if (precomputedDag !== undefined) {
|
|
363
|
+
dag = precomputedDag;
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
const res = (0, run_dag_js_1.projectRunDagV2)(events);
|
|
367
|
+
dag = res.isOk() ? res.value : null;
|
|
368
|
+
}
|
|
369
|
+
if (dag === null)
|
|
341
370
|
return null;
|
|
342
|
-
const
|
|
343
|
-
const statusRes = (0, run_status_signals_js_1.projectRunStatusSignalsV2)(
|
|
344
|
-
const gapsRes = (0, gaps_js_1.projectGapsV2)(
|
|
345
|
-
const sessionTitle = deriveSessionTitle(
|
|
371
|
+
const sortedEventsRes = (0, sorted_event_log_js_1.asSortedEventLog)(events);
|
|
372
|
+
const statusRes = sortedEventsRes.isOk() ? (0, run_status_signals_js_1.projectRunStatusSignalsV2)(sortedEventsRes.value) : (0, neverthrow_2.err)(sortedEventsRes.error);
|
|
373
|
+
const gapsRes = sortedEventsRes.isOk() ? (0, gaps_js_1.projectGapsV2)(sortedEventsRes.value) : (0, neverthrow_2.err)(sortedEventsRes.error);
|
|
374
|
+
const sessionTitle = sortedEventsRes.isOk() ? deriveSessionTitle(sortedEventsRes.value) : null;
|
|
346
375
|
const gitBranch = extractGitBranch(events);
|
|
347
376
|
const repoRoot = extractRepoRoot(events);
|
|
348
377
|
const runs = Object.values(dag.runsById);
|
|
@@ -415,26 +444,52 @@ function projectSessionDetail(sessionId, truth, completionByRunId, stepLabels, w
|
|
|
415
444
|
const { events } = truth;
|
|
416
445
|
const health = (0, session_health_js_1.projectSessionHealthV2)(truth);
|
|
417
446
|
const sessionHealth = health.isOk() && health.value.kind === 'healthy' ? 'healthy' : 'corrupt';
|
|
418
|
-
const
|
|
447
|
+
const sortedEventsRes = (0, sorted_event_log_js_1.asSortedEventLog)(events);
|
|
448
|
+
const sessionTitle = sortedEventsRes.isOk() ? deriveSessionTitle(sortedEventsRes.value) : null;
|
|
419
449
|
const dagRes = (0, run_dag_js_1.projectRunDagV2)(events);
|
|
420
450
|
if (dagRes.isErr()) {
|
|
421
451
|
return { sessionId, sessionTitle, health: sessionHealth, runs: [] };
|
|
422
452
|
}
|
|
423
|
-
const statusRes = (0, run_status_signals_js_1.projectRunStatusSignalsV2)(
|
|
424
|
-
const gapsRes = (0, gaps_js_1.projectGapsV2)(
|
|
453
|
+
const statusRes = sortedEventsRes.isOk() ? (0, run_status_signals_js_1.projectRunStatusSignalsV2)(sortedEventsRes.value) : (0, neverthrow_2.err)(sortedEventsRes.error);
|
|
454
|
+
const gapsRes = sortedEventsRes.isOk() ? (0, gaps_js_1.projectGapsV2)(sortedEventsRes.value) : (0, neverthrow_2.err)(sortedEventsRes.error);
|
|
455
|
+
const executionTraceRes = (0, run_execution_trace_js_1.projectRunExecutionTraceV2)(events);
|
|
456
|
+
const outputsRes = (0, node_outputs_js_1.projectNodeOutputsV2)(events);
|
|
457
|
+
const artifactsRes = (0, artifacts_js_1.projectArtifactsV2)(events);
|
|
458
|
+
const failedValidationNodeIds = new Set();
|
|
459
|
+
for (const e of events) {
|
|
460
|
+
if (e.kind !== constants_js_1.EVENT_KIND.VALIDATION_PERFORMED)
|
|
461
|
+
continue;
|
|
462
|
+
if (!e.data.result.valid) {
|
|
463
|
+
failedValidationNodeIds.add(e.scope.nodeId);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
const gapNodeIds = new Set();
|
|
467
|
+
if (gapsRes.isOk()) {
|
|
468
|
+
for (const gap of Object.values(gapsRes.value.byGapId)) {
|
|
469
|
+
gapNodeIds.add(gap.nodeId);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
425
472
|
const runs = Object.values(dagRes.value.runsById).map((run) => {
|
|
426
473
|
const statusSignals = statusRes.isOk() ? statusRes.value.byRunId[run.runId] : undefined;
|
|
427
474
|
const status = deriveRunStatus(statusSignals?.isBlocked ?? false, statusSignals?.hasUnresolvedCriticalGaps ?? false, completionByRunId[run.runId] ?? false);
|
|
428
475
|
const tipSet = new Set(run.tipNodeIds);
|
|
429
|
-
const nodes = Object.values(run.nodesById).map((node) =>
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
476
|
+
const nodes = Object.values(run.nodesById).map((node) => {
|
|
477
|
+
const nodeOutputs = outputsRes.isOk() ? outputsRes.value.nodesById[node.nodeId] : undefined;
|
|
478
|
+
const nodeArtifacts = artifactsRes.isOk() ? artifactsRes.value.byNodeId[node.nodeId] : undefined;
|
|
479
|
+
return {
|
|
480
|
+
nodeId: node.nodeId,
|
|
481
|
+
nodeKind: node.nodeKind,
|
|
482
|
+
parentNodeId: node.parentNodeId,
|
|
483
|
+
createdAtEventIndex: node.createdAtEventIndex,
|
|
484
|
+
isPreferredTip: node.nodeId === run.preferredTipNodeId,
|
|
485
|
+
isTip: tipSet.has(node.nodeId),
|
|
486
|
+
stepLabel: stepLabels[node.nodeId] ?? null,
|
|
487
|
+
hasRecap: (nodeOutputs?.currentByChannel.recap.length ?? 0) > 0,
|
|
488
|
+
hasFailedValidations: failedValidationNodeIds.has(node.nodeId),
|
|
489
|
+
hasGaps: gapNodeIds.has(node.nodeId),
|
|
490
|
+
hasArtifacts: (nodeArtifacts?.artifacts.length ?? 0) > 0,
|
|
491
|
+
};
|
|
492
|
+
});
|
|
438
493
|
const edges = run.edges.map((edge) => ({
|
|
439
494
|
edgeKind: edge.edgeKind,
|
|
440
495
|
fromNodeId: edge.fromNodeId,
|
|
@@ -456,6 +511,9 @@ function projectSessionDetail(sessionId, truth, completionByRunId, stepLabels, w
|
|
|
456
511
|
hasUnresolvedCriticalGaps: gapsRes.isOk()
|
|
457
512
|
? (gapsRes.value.unresolvedCriticalByRunId[run.runId]?.length ?? 0) > 0
|
|
458
513
|
: false,
|
|
514
|
+
executionTraceSummary: executionTraceRes.isOk()
|
|
515
|
+
? (executionTraceRes.value.byRunId[run.runId] ?? null)
|
|
516
|
+
: null,
|
|
459
517
|
};
|
|
460
518
|
});
|
|
461
519
|
return { sessionId, sessionTitle, health: sessionHealth, runs };
|
|
@@ -563,7 +621,10 @@ function extractValidations(events, nodeId) {
|
|
|
563
621
|
return results;
|
|
564
622
|
}
|
|
565
623
|
function extractGaps(events, nodeId) {
|
|
566
|
-
const
|
|
624
|
+
const sortedEventsRes = (0, sorted_event_log_js_1.asSortedEventLog)(events);
|
|
625
|
+
if (sortedEventsRes.isErr())
|
|
626
|
+
return [];
|
|
627
|
+
const gapsRes = (0, gaps_js_1.projectGapsV2)(sortedEventsRes.value);
|
|
567
628
|
if (gapsRes.isErr())
|
|
568
629
|
return [];
|
|
569
630
|
const gaps = [];
|
|
@@ -31,6 +31,10 @@ export interface ConsoleDagNode {
|
|
|
31
31
|
readonly isPreferredTip: boolean;
|
|
32
32
|
readonly isTip: boolean;
|
|
33
33
|
readonly stepLabel: string | null;
|
|
34
|
+
readonly hasRecap: boolean;
|
|
35
|
+
readonly hasFailedValidations: boolean;
|
|
36
|
+
readonly hasGaps: boolean;
|
|
37
|
+
readonly hasArtifacts: boolean;
|
|
34
38
|
}
|
|
35
39
|
export interface ConsoleDagEdge {
|
|
36
40
|
readonly edgeKind: 'acked_step' | 'checkpoint';
|
|
@@ -38,6 +42,25 @@ export interface ConsoleDagEdge {
|
|
|
38
42
|
readonly toNodeId: string;
|
|
39
43
|
readonly createdAtEventIndex: number;
|
|
40
44
|
}
|
|
45
|
+
export type ConsoleExecutionTraceItemKind = 'selected_next_step' | 'evaluated_condition' | 'entered_loop' | 'exited_loop' | 'detected_non_tip_advance' | 'context_fact' | 'divergence';
|
|
46
|
+
export interface ConsoleExecutionTraceRef {
|
|
47
|
+
readonly kind: 'node_id' | 'step_id' | 'loop_id' | 'condition_id';
|
|
48
|
+
readonly value: string;
|
|
49
|
+
}
|
|
50
|
+
export interface ConsoleExecutionTraceItem {
|
|
51
|
+
readonly kind: ConsoleExecutionTraceItemKind;
|
|
52
|
+
readonly summary: string;
|
|
53
|
+
readonly recordedAtEventIndex: number;
|
|
54
|
+
readonly refs: readonly ConsoleExecutionTraceRef[];
|
|
55
|
+
}
|
|
56
|
+
export interface ConsoleExecutionTraceFact {
|
|
57
|
+
readonly key: string;
|
|
58
|
+
readonly value: string;
|
|
59
|
+
}
|
|
60
|
+
export interface ConsoleExecutionTraceSummary {
|
|
61
|
+
readonly items: readonly ConsoleExecutionTraceItem[];
|
|
62
|
+
readonly contextFacts: readonly ConsoleExecutionTraceFact[];
|
|
63
|
+
}
|
|
41
64
|
export interface ConsoleDagRun {
|
|
42
65
|
readonly runId: string;
|
|
43
66
|
readonly workflowId: string | null;
|
|
@@ -49,6 +72,7 @@ export interface ConsoleDagRun {
|
|
|
49
72
|
readonly tipNodeIds: readonly string[];
|
|
50
73
|
readonly status: ConsoleRunStatus;
|
|
51
74
|
readonly hasUnresolvedCriticalGaps: boolean;
|
|
75
|
+
readonly executionTraceSummary: ConsoleExecutionTraceSummary | null;
|
|
52
76
|
}
|
|
53
77
|
export interface ConsoleSessionDetail {
|
|
54
78
|
readonly sessionId: string;
|
|
@@ -83,6 +107,11 @@ export interface ConsoleArtifact {
|
|
|
83
107
|
readonly byteLength: number;
|
|
84
108
|
readonly content: unknown;
|
|
85
109
|
}
|
|
110
|
+
export type FileChangeStatus = 'modified' | 'added' | 'deleted' | 'untracked' | 'renamed' | 'other';
|
|
111
|
+
export interface ChangedFile {
|
|
112
|
+
readonly status: FileChangeStatus;
|
|
113
|
+
readonly path: string;
|
|
114
|
+
}
|
|
86
115
|
export interface ConsoleWorktreeSummary {
|
|
87
116
|
readonly path: string;
|
|
88
117
|
readonly name: string;
|
|
@@ -91,8 +120,15 @@ export interface ConsoleWorktreeSummary {
|
|
|
91
120
|
readonly headMessage: string;
|
|
92
121
|
readonly headTimestampMs: number;
|
|
93
122
|
readonly changedCount: number;
|
|
123
|
+
readonly changedFiles: readonly ChangedFile[];
|
|
94
124
|
readonly aheadCount: number;
|
|
125
|
+
readonly unpushedCommits: readonly {
|
|
126
|
+
readonly hash: string;
|
|
127
|
+
readonly message: string;
|
|
128
|
+
}[];
|
|
129
|
+
readonly isMerged: boolean;
|
|
95
130
|
readonly activeSessionCount: number;
|
|
131
|
+
readonly description?: string;
|
|
96
132
|
}
|
|
97
133
|
export interface ConsoleRepoWorktrees {
|
|
98
134
|
readonly repoName: string;
|
|
@@ -116,3 +152,32 @@ export interface ConsoleNodeDetail {
|
|
|
116
152
|
readonly validations: readonly ConsoleValidationResult[];
|
|
117
153
|
readonly gaps: readonly ConsoleNodeGap[];
|
|
118
154
|
}
|
|
155
|
+
export interface ConsoleWorkflowSourceInfo {
|
|
156
|
+
readonly kind: 'bundled' | 'user' | 'project' | 'custom' | 'git' | 'remote' | 'plugin';
|
|
157
|
+
readonly displayName: string;
|
|
158
|
+
}
|
|
159
|
+
export interface ConsoleWorkflowSummary {
|
|
160
|
+
readonly id: string;
|
|
161
|
+
readonly name: string;
|
|
162
|
+
readonly description: string;
|
|
163
|
+
readonly version: string;
|
|
164
|
+
readonly tags: readonly string[];
|
|
165
|
+
readonly source: ConsoleWorkflowSourceInfo;
|
|
166
|
+
readonly about?: string;
|
|
167
|
+
readonly examples?: readonly string[];
|
|
168
|
+
}
|
|
169
|
+
export interface ConsoleWorkflowListResponse {
|
|
170
|
+
readonly workflows: readonly ConsoleWorkflowSummary[];
|
|
171
|
+
}
|
|
172
|
+
export interface ConsoleWorkflowDetail {
|
|
173
|
+
readonly id: string;
|
|
174
|
+
readonly name: string;
|
|
175
|
+
readonly description: string;
|
|
176
|
+
readonly version: string;
|
|
177
|
+
readonly tags: readonly string[];
|
|
178
|
+
readonly source: ConsoleWorkflowSourceInfo;
|
|
179
|
+
readonly stepCount: number;
|
|
180
|
+
readonly about?: string;
|
|
181
|
+
readonly examples?: readonly string[];
|
|
182
|
+
readonly preconditions?: readonly string[];
|
|
183
|
+
}
|
|
@@ -44,32 +44,107 @@ function parseWorktreePorcelain(raw) {
|
|
|
44
44
|
}
|
|
45
45
|
return entries;
|
|
46
46
|
}
|
|
47
|
+
const MAX_CONCURRENT_ENRICHMENTS = 8;
|
|
48
|
+
let activeEnrichments = 0;
|
|
49
|
+
const enrichmentQueue = [];
|
|
50
|
+
function acquireEnrichmentSlot() {
|
|
51
|
+
return new Promise((resolve) => {
|
|
52
|
+
if (activeEnrichments < MAX_CONCURRENT_ENRICHMENTS) {
|
|
53
|
+
activeEnrichments++;
|
|
54
|
+
resolve();
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
enrichmentQueue.push(() => { activeEnrichments++; resolve(); });
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function releaseEnrichmentSlot() {
|
|
62
|
+
const next = enrichmentQueue.shift();
|
|
63
|
+
if (next) {
|
|
64
|
+
next();
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
activeEnrichments--;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function parseFileStatus(xy) {
|
|
71
|
+
if (xy === '??')
|
|
72
|
+
return 'untracked';
|
|
73
|
+
const x = xy[0] ?? ' ';
|
|
74
|
+
const y = xy[1] ?? ' ';
|
|
75
|
+
if (x === 'R')
|
|
76
|
+
return 'renamed';
|
|
77
|
+
if (x === 'A')
|
|
78
|
+
return 'added';
|
|
79
|
+
if (x === 'D' || y === 'D')
|
|
80
|
+
return 'deleted';
|
|
81
|
+
if (x === 'M' || y === 'M')
|
|
82
|
+
return 'modified';
|
|
83
|
+
return 'other';
|
|
84
|
+
}
|
|
85
|
+
function parseChangedFiles(statusRaw) {
|
|
86
|
+
if (!statusRaw)
|
|
87
|
+
return [];
|
|
88
|
+
return statusRaw
|
|
89
|
+
.split('\n')
|
|
90
|
+
.filter(line => line.trim().length > 0)
|
|
91
|
+
.map(line => ({
|
|
92
|
+
status: parseFileStatus(line.slice(0, 2)),
|
|
93
|
+
path: line.slice(3),
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
const MAIN_BRANCH_REF = 'origin/main';
|
|
47
97
|
async function enrichWorktree(wt) {
|
|
48
|
-
const
|
|
98
|
+
const descriptionKey = wt.branch ? `branch.${wt.branch}.description` : null;
|
|
99
|
+
const [logRaw, statusRaw, aheadRaw, descriptionRaw, unpushedLogRaw, mergedBranchesRaw] = await Promise.all([
|
|
49
100
|
git(wt.path, ['log', '-1', '--format=%h%n%s%n%ct']),
|
|
50
101
|
git(wt.path, ['status', '--short']),
|
|
51
|
-
git(wt.path, ['rev-list', '--count',
|
|
102
|
+
git(wt.path, ['rev-list', '--count', `${MAIN_BRANCH_REF}..HEAD`]),
|
|
103
|
+
descriptionKey ? git(wt.path, ['config', descriptionKey]) : Promise.resolve(null),
|
|
104
|
+
git(wt.path, ['log', `${MAIN_BRANCH_REF}..HEAD`, '--oneline']),
|
|
105
|
+
wt.branch && wt.branch !== 'main' ? git(wt.path, ['branch', '--merged', MAIN_BRANCH_REF]) : Promise.resolve(null),
|
|
52
106
|
]);
|
|
53
107
|
const [hashLine, messageLine, timestampLine] = logRaw?.split('\n') ?? [];
|
|
54
108
|
const headHash = hashLine?.trim() || wt.head.slice(0, 7);
|
|
55
109
|
const headMessage = messageLine?.trim() ?? '';
|
|
56
110
|
const headTimestampMs = timestampLine ? parseInt(timestampLine.trim(), 10) * 1000 : 0;
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
: 0;
|
|
111
|
+
const changedFiles = statusRaw !== null ? parseChangedFiles(statusRaw) : [];
|
|
112
|
+
const changedCount = changedFiles.length;
|
|
60
113
|
const parsedAhead = aheadRaw !== null ? parseInt(aheadRaw, 10) : NaN;
|
|
61
114
|
const aheadCount = isNaN(parsedAhead) ? 0 : parsedAhead;
|
|
62
|
-
|
|
115
|
+
const unpushedCommits = unpushedLogRaw
|
|
116
|
+
? unpushedLogRaw.split('\n').filter(l => l.trim().length > 0).map(line => ({
|
|
117
|
+
hash: line.slice(0, 7),
|
|
118
|
+
message: line.slice(8),
|
|
119
|
+
}))
|
|
120
|
+
: [];
|
|
121
|
+
const isMerged = wt.branch !== null &&
|
|
122
|
+
wt.branch !== 'main' &&
|
|
123
|
+
mergedBranchesRaw !== null &&
|
|
124
|
+
mergedBranchesRaw.split('\n').some(line => line.trim() === wt.branch);
|
|
125
|
+
const branchDescription = descriptionRaw?.trim() ?? '';
|
|
126
|
+
return { headHash, headMessage, headTimestampMs, changedCount, changedFiles, aheadCount, unpushedCommits, isMerged, branchDescription };
|
|
63
127
|
}
|
|
64
128
|
async function resolveRepoRoot(path) {
|
|
65
|
-
|
|
129
|
+
const commonDir = await git(path, ['rev-parse', '--path-format=absolute', '--git-common-dir']);
|
|
130
|
+
if (commonDir === null)
|
|
131
|
+
return null;
|
|
132
|
+
return commonDir.replace(/\/\.git\/?$/, '') || null;
|
|
66
133
|
}
|
|
67
134
|
async function enrichRepo(repoRoot, activeSessions) {
|
|
68
135
|
const porcelain = await git(repoRoot, ['worktree', 'list', '--porcelain']);
|
|
69
136
|
if (porcelain === null)
|
|
70
137
|
return null;
|
|
71
138
|
const rawWorktrees = parseWorktreePorcelain(porcelain);
|
|
72
|
-
const results = await Promise.allSettled(rawWorktrees.map(wt =>
|
|
139
|
+
const results = await Promise.allSettled(rawWorktrees.map(async (wt) => {
|
|
140
|
+
await acquireEnrichmentSlot();
|
|
141
|
+
try {
|
|
142
|
+
return await enrichWorktree(wt);
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
releaseEnrichmentSlot();
|
|
146
|
+
}
|
|
147
|
+
}));
|
|
73
148
|
const worktrees = rawWorktrees.flatMap((wt, i) => {
|
|
74
149
|
const result = results[i];
|
|
75
150
|
if (result.status === 'rejected') {
|
|
@@ -85,8 +160,12 @@ async function enrichRepo(repoRoot, activeSessions) {
|
|
|
85
160
|
headMessage: e.headMessage,
|
|
86
161
|
headTimestampMs: e.headTimestampMs,
|
|
87
162
|
changedCount: e.changedCount,
|
|
163
|
+
changedFiles: e.changedFiles,
|
|
88
164
|
aheadCount: e.aheadCount,
|
|
165
|
+
unpushedCommits: e.unpushedCommits,
|
|
166
|
+
isMerged: e.isMerged,
|
|
89
167
|
activeSessionCount: wt.branch ? (activeSessions.counts.get(wt.branch) ?? 0) : 0,
|
|
168
|
+
...(e.branchDescription ? { description: e.branchDescription } : {}),
|
|
90
169
|
}];
|
|
91
170
|
});
|
|
92
171
|
return [...worktrees].sort((a, b) => {
|