@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.
Files changed (156) hide show
  1. package/dist/application/services/validation-engine.js +4 -9
  2. package/dist/application/services/workflow-compiler.js +4 -6
  3. package/dist/application/services/workflow-service.d.ts +2 -0
  4. package/dist/application/services/workflow-service.js +3 -0
  5. package/dist/console/assets/index-BE5PAgPO.js +28 -0
  6. package/dist/console/assets/index-BZNM03t1.css +1 -0
  7. package/dist/console/index.html +2 -2
  8. package/dist/engine/engine-factory.js +2 -2
  9. package/dist/engine/types.d.ts +1 -1
  10. package/dist/env-flags.d.ts +1 -0
  11. package/dist/env-flags.js +4 -0
  12. package/dist/infrastructure/session/HttpServer.d.ts +3 -3
  13. package/dist/infrastructure/session/HttpServer.js +68 -74
  14. package/dist/infrastructure/storage/caching-workflow-storage.d.ts +2 -0
  15. package/dist/infrastructure/storage/caching-workflow-storage.js +15 -6
  16. package/dist/infrastructure/storage/file-workflow-storage.js +3 -4
  17. package/dist/infrastructure/storage/schema-validating-workflow-storage.js +9 -8
  18. package/dist/manifest.json +283 -219
  19. package/dist/mcp/assert-output.d.ts +37 -0
  20. package/dist/mcp/assert-output.js +52 -0
  21. package/dist/mcp/boundary-coercion.d.ts +1 -0
  22. package/dist/mcp/boundary-coercion.js +44 -0
  23. package/dist/mcp/dev-mode.d.ts +1 -0
  24. package/dist/mcp/dev-mode.js +4 -0
  25. package/dist/mcp/handler-factory.js +12 -9
  26. package/dist/mcp/handlers/session.js +8 -9
  27. package/dist/mcp/handlers/shared/request-workflow-reader.d.ts +5 -0
  28. package/dist/mcp/handlers/shared/request-workflow-reader.js +47 -2
  29. package/dist/mcp/handlers/v2-advance-core/assessment-consequences.d.ts +1 -1
  30. package/dist/mcp/handlers/v2-advance-core/assessment-consequences.js +4 -5
  31. package/dist/mcp/handlers/v2-advance-core/event-builders.d.ts +2 -0
  32. package/dist/mcp/handlers/v2-advance-core/event-builders.js +6 -6
  33. package/dist/mcp/handlers/v2-advance-core/index.d.ts +2 -0
  34. package/dist/mcp/handlers/v2-advance-core/index.js +5 -4
  35. package/dist/mcp/handlers/v2-advance-core/input-validation.d.ts +2 -0
  36. package/dist/mcp/handlers/v2-advance-core/input-validation.js +32 -9
  37. package/dist/mcp/handlers/v2-advance-core/outcome-blocked.d.ts +2 -0
  38. package/dist/mcp/handlers/v2-advance-core/outcome-blocked.js +2 -2
  39. package/dist/mcp/handlers/v2-advance-core/outcome-success.d.ts +2 -0
  40. package/dist/mcp/handlers/v2-advance-core/outcome-success.js +1 -1
  41. package/dist/mcp/handlers/v2-checkpoint.d.ts +1 -1
  42. package/dist/mcp/handlers/v2-checkpoint.js +5 -6
  43. package/dist/mcp/handlers/v2-execution/advance.d.ts +4 -2
  44. package/dist/mcp/handlers/v2-execution/advance.js +5 -7
  45. package/dist/mcp/handlers/v2-execution/continue-advance.js +56 -26
  46. package/dist/mcp/handlers/v2-execution/continue-rehydrate.d.ts +1 -1
  47. package/dist/mcp/handlers/v2-execution/continue-rehydrate.js +9 -9
  48. package/dist/mcp/handlers/v2-execution/replay.d.ts +6 -4
  49. package/dist/mcp/handlers/v2-execution/replay.js +47 -30
  50. package/dist/mcp/handlers/v2-execution/start.d.ts +3 -3
  51. package/dist/mcp/handlers/v2-execution/start.js +31 -12
  52. package/dist/mcp/handlers/v2-execution/workflow-object-cache.d.ts +5 -0
  53. package/dist/mcp/handlers/v2-execution/workflow-object-cache.js +19 -0
  54. package/dist/mcp/handlers/v2-execution-helpers.d.ts +1 -0
  55. package/dist/mcp/handlers/v2-execution-helpers.js +23 -7
  56. package/dist/mcp/handlers/v2-resume.d.ts +1 -1
  57. package/dist/mcp/handlers/v2-resume.js +3 -4
  58. package/dist/mcp/handlers/v2-state-conversion.js +5 -1
  59. package/dist/mcp/handlers/v2-workflow.d.ts +100 -0
  60. package/dist/mcp/handlers/v2-workflow.js +155 -31
  61. package/dist/mcp/handlers/workflow.d.ts +2 -5
  62. package/dist/mcp/handlers/workflow.js +15 -12
  63. package/dist/mcp/output-schemas.d.ts +123 -29
  64. package/dist/mcp/output-schemas.js +36 -18
  65. package/dist/mcp/server.js +70 -5
  66. package/dist/mcp/tool-call-timing.d.ts +24 -0
  67. package/dist/mcp/tool-call-timing.js +85 -0
  68. package/dist/mcp/tool-descriptions.js +17 -9
  69. package/dist/mcp/transports/http-entry.js +3 -2
  70. package/dist/mcp/transports/http-listener.d.ts +1 -0
  71. package/dist/mcp/transports/http-listener.js +25 -0
  72. package/dist/mcp/transports/shutdown-hooks.d.ts +4 -1
  73. package/dist/mcp/transports/shutdown-hooks.js +3 -2
  74. package/dist/mcp/transports/stdio-entry.js +6 -28
  75. package/dist/mcp/v2/tools.d.ts +6 -0
  76. package/dist/mcp/v2/tools.js +2 -0
  77. package/dist/mcp/v2-response-formatter.js +2 -4
  78. package/dist/mcp/validation/schema-introspection.d.ts +1 -0
  79. package/dist/mcp/validation/schema-introspection.js +15 -5
  80. package/dist/mcp/validation/suggestion-generator.js +2 -2
  81. package/dist/mcp/workflow-protocol-contracts.js +5 -1
  82. package/dist/runtime/adapters/node-process-signals.d.ts +1 -0
  83. package/dist/runtime/adapters/node-process-signals.js +5 -0
  84. package/dist/runtime/adapters/noop-process-signals.d.ts +1 -0
  85. package/dist/runtime/adapters/noop-process-signals.js +2 -0
  86. package/dist/runtime/ports/process-signals.d.ts +1 -0
  87. package/dist/types/workflow-definition.d.ts +3 -2
  88. package/dist/types/workflow.d.ts +3 -0
  89. package/dist/types/workflow.js +35 -26
  90. package/dist/v2/durable-core/domain/context-template-resolver.js +2 -2
  91. package/dist/v2/durable-core/domain/function-definition-expander.js +2 -17
  92. package/dist/v2/durable-core/domain/prompt-renderer.d.ts +1 -0
  93. package/dist/v2/durable-core/domain/prompt-renderer.js +23 -18
  94. package/dist/v2/durable-core/domain/recap-recovery.js +23 -16
  95. package/dist/v2/durable-core/domain/retrieval-contract.js +13 -7
  96. package/dist/v2/durable-core/session-index.d.ts +22 -0
  97. package/dist/v2/durable-core/session-index.js +58 -0
  98. package/dist/v2/durable-core/sorted-event-log.d.ts +6 -0
  99. package/dist/v2/durable-core/sorted-event-log.js +15 -0
  100. package/dist/v2/infra/local/fs/index.js +8 -8
  101. package/dist/v2/infra/local/session-store/index.d.ts +1 -1
  102. package/dist/v2/infra/local/session-store/index.js +71 -61
  103. package/dist/v2/infra/local/session-summary-provider/index.js +9 -4
  104. package/dist/v2/infra/local/snapshot-store/index.js +2 -1
  105. package/dist/v2/infra/local/workspace-anchor/index.js +4 -1
  106. package/dist/v2/ports/session-event-log-store.port.d.ts +1 -1
  107. package/dist/v2/projections/assessment-consequences.d.ts +2 -1
  108. package/dist/v2/projections/assessment-consequences.js +0 -5
  109. package/dist/v2/projections/assessments.d.ts +2 -1
  110. package/dist/v2/projections/assessments.js +2 -4
  111. package/dist/v2/projections/gaps.d.ts +2 -1
  112. package/dist/v2/projections/gaps.js +0 -5
  113. package/dist/v2/projections/preferences.d.ts +2 -1
  114. package/dist/v2/projections/preferences.js +0 -5
  115. package/dist/v2/projections/run-context.d.ts +2 -2
  116. package/dist/v2/projections/run-context.js +0 -5
  117. package/dist/v2/projections/run-dag.js +7 -1
  118. package/dist/v2/projections/run-execution-trace.d.ts +8 -0
  119. package/dist/v2/projections/run-execution-trace.js +124 -0
  120. package/dist/v2/projections/run-status-signals.d.ts +2 -2
  121. package/dist/v2/usecases/console-routes.d.ts +3 -1
  122. package/dist/v2/usecases/console-routes.js +149 -3
  123. package/dist/v2/usecases/console-service.d.ts +2 -0
  124. package/dist/v2/usecases/console-service.js +87 -26
  125. package/dist/v2/usecases/console-types.d.ts +65 -0
  126. package/dist/v2/usecases/worktree-service.js +87 -8
  127. package/package.json +7 -6
  128. package/spec/authoring-spec.json +82 -1
  129. package/spec/workflow-tags.json +132 -0
  130. package/spec/workflow.schema.json +21 -11
  131. package/workflows/adaptive-ticket-creation.json +33 -8
  132. package/workflows/architecture-scalability-audit.json +50 -9
  133. package/workflows/bug-investigation.agentic.v2.json +43 -14
  134. package/workflows/coding-task-workflow-agentic.json +57 -38
  135. package/workflows/coding-task-workflow-agentic.lean.v2.json +129 -34
  136. package/workflows/coding-task-workflow-agentic.v2.json +97 -30
  137. package/workflows/cross-platform-code-conversion.v2.json +175 -48
  138. package/workflows/document-creation-workflow.json +49 -12
  139. package/workflows/documentation-update-workflow.json +9 -2
  140. package/workflows/intelligent-test-case-generation.json +9 -2
  141. package/workflows/learner-centered-course-workflow.json +273 -266
  142. package/workflows/mr-review-workflow.agentic.v2.json +88 -14
  143. package/workflows/personal-learning-materials-creation-branched.json +181 -174
  144. package/workflows/presentation-creation.json +167 -160
  145. package/workflows/production-readiness-audit.json +61 -15
  146. package/workflows/relocation-workflow-us.json +21 -5
  147. package/workflows/routines/tension-driven-design.json +1 -1
  148. package/workflows/scoped-documentation-workflow.json +9 -2
  149. package/workflows/test-artifact-loop-control.json +1 -2
  150. package/workflows/ui-ux-design-workflow.json +334 -0
  151. package/workflows/workflow-diagnose-environment.json +7 -1
  152. package/workflows/workflow-for-workflows.json +514 -484
  153. package/workflows/workflow-for-workflows.v2.json +55 -11
  154. package/workflows/wr.discovery.json +118 -29
  155. package/dist/console/assets/index-DW78t31j.css +0 -1
  156. 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
- function mountConsoleRoutes(app, consoleService) {
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 repoRootSet = new Set(sessions.map(s => s.repoRoot).filter((r) => r !== null));
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
- resolveRunCompletion(truth.events, this.ports.snapshotStore),
124
+ completionRA,
112
125
  workflowNamesRA,
113
- ]).map(([completionMap, workflowNames]) => projectSessionSummary(sessionId, truth, completionMap, workflowNames, lastModifiedMs, nowMs));
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(events) {
240
- const contextRes = (0, run_context_js_1.projectRunContextV2)(events);
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 truncateTitle(val.trim());
268
+ return val.trim();
247
269
  }
248
270
  }
249
271
  }
250
272
  }
251
- const title = extractTitleFromFirstRecap(events);
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
- const dagRes = (0, run_dag_js_1.projectRunDagV2)(events);
340
- if (dagRes.isErr())
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 dag = dagRes.value;
343
- const statusRes = (0, run_status_signals_js_1.projectRunStatusSignalsV2)(events);
344
- const gapsRes = (0, gaps_js_1.projectGapsV2)(events);
345
- const sessionTitle = deriveSessionTitle(events);
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 sessionTitle = deriveSessionTitle(events);
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)(events);
424
- const gapsRes = (0, gaps_js_1.projectGapsV2)(events);
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
- nodeId: node.nodeId,
431
- nodeKind: node.nodeKind,
432
- parentNodeId: node.parentNodeId,
433
- createdAtEventIndex: node.createdAtEventIndex,
434
- isPreferredTip: node.nodeId === run.preferredTipNodeId,
435
- isTip: tipSet.has(node.nodeId),
436
- stepLabel: stepLabels[node.nodeId] ?? null,
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 gapsRes = (0, gaps_js_1.projectGapsV2)(events);
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 [logRaw, statusRaw, aheadRaw] = await Promise.all([
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', 'origin/main..HEAD']),
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 changedCount = statusRaw !== null
58
- ? statusRaw.split('\n').filter(l => l.trim()).length
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
- return { headHash, headMessage, headTimestampMs, changedCount, aheadCount };
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
- return git(path, ['rev-parse', '--show-toplevel']);
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 => enrichWorktree(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) => {