@exaudeus/workrail 3.15.0 → 3.17.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 (164) hide show
  1. package/dist/application/services/workflow-service.d.ts +2 -0
  2. package/dist/application/services/workflow-service.js +3 -0
  3. package/dist/application/use-cases/raw-workflow-file-scanner.js +10 -13
  4. package/dist/cli/commands/index.d.ts +1 -1
  5. package/dist/cli/commands/index.js +2 -1
  6. package/dist/cli/commands/init.d.ts +10 -0
  7. package/dist/cli/commands/init.js +72 -0
  8. package/dist/cli.js +13 -1
  9. package/dist/config/config-file.d.ts +8 -0
  10. package/dist/config/config-file.js +141 -0
  11. package/dist/config/feature-flags.js +8 -0
  12. package/dist/console/assets/index-BZNM03t1.css +1 -0
  13. package/dist/console/assets/index-BwJelCXK.js +28 -0
  14. package/dist/console/index.html +2 -2
  15. package/dist/di/container.d.ts +1 -0
  16. package/dist/di/container.js +24 -7
  17. package/dist/infrastructure/session/HttpServer.d.ts +3 -4
  18. package/dist/infrastructure/session/HttpServer.js +58 -106
  19. package/dist/infrastructure/storage/caching-workflow-storage.d.ts +2 -0
  20. package/dist/infrastructure/storage/caching-workflow-storage.js +15 -6
  21. package/dist/infrastructure/storage/file-workflow-storage.js +3 -4
  22. package/dist/infrastructure/storage/schema-validating-workflow-storage.js +9 -8
  23. package/dist/manifest.json +303 -247
  24. package/dist/mcp/assert-output.d.ts +37 -0
  25. package/dist/mcp/assert-output.js +53 -0
  26. package/dist/mcp/boundary-coercion.d.ts +1 -0
  27. package/dist/mcp/boundary-coercion.js +44 -0
  28. package/dist/mcp/dev-mode.d.ts +2 -0
  29. package/dist/mcp/dev-mode.js +16 -0
  30. package/dist/mcp/handler-factory.d.ts +1 -1
  31. package/dist/mcp/handler-factory.js +20 -16
  32. package/dist/mcp/handlers/session.js +8 -9
  33. package/dist/mcp/handlers/shared/request-workflow-reader.d.ts +1 -0
  34. package/dist/mcp/handlers/shared/request-workflow-reader.js +90 -20
  35. package/dist/mcp/handlers/v2-advance-core/event-builders.d.ts +2 -0
  36. package/dist/mcp/handlers/v2-advance-core/event-builders.js +6 -6
  37. package/dist/mcp/handlers/v2-advance-core/index.d.ts +2 -0
  38. package/dist/mcp/handlers/v2-advance-core/index.js +4 -3
  39. package/dist/mcp/handlers/v2-advance-core/input-validation.d.ts +2 -0
  40. package/dist/mcp/handlers/v2-advance-core/input-validation.js +32 -9
  41. package/dist/mcp/handlers/v2-advance-core/outcome-blocked.d.ts +2 -0
  42. package/dist/mcp/handlers/v2-advance-core/outcome-blocked.js +1 -1
  43. package/dist/mcp/handlers/v2-advance-core/outcome-success.d.ts +2 -0
  44. package/dist/mcp/handlers/v2-advance-core/outcome-success.js +1 -1
  45. package/dist/mcp/handlers/v2-checkpoint.d.ts +1 -1
  46. package/dist/mcp/handlers/v2-checkpoint.js +5 -6
  47. package/dist/mcp/handlers/v2-execution/advance.d.ts +4 -2
  48. package/dist/mcp/handlers/v2-execution/advance.js +5 -7
  49. package/dist/mcp/handlers/v2-execution/continue-advance.d.ts +1 -0
  50. package/dist/mcp/handlers/v2-execution/continue-advance.js +59 -27
  51. package/dist/mcp/handlers/v2-execution/continue-rehydrate.d.ts +2 -1
  52. package/dist/mcp/handlers/v2-execution/continue-rehydrate.js +11 -10
  53. package/dist/mcp/handlers/v2-execution/index.js +2 -0
  54. package/dist/mcp/handlers/v2-execution/replay.d.ts +8 -4
  55. package/dist/mcp/handlers/v2-execution/replay.js +50 -30
  56. package/dist/mcp/handlers/v2-execution/start.d.ts +2 -3
  57. package/dist/mcp/handlers/v2-execution/start.js +58 -30
  58. package/dist/mcp/handlers/v2-execution/workflow-object-cache.d.ts +5 -0
  59. package/dist/mcp/handlers/v2-execution/workflow-object-cache.js +19 -0
  60. package/dist/mcp/handlers/v2-execution-helpers.d.ts +1 -0
  61. package/dist/mcp/handlers/v2-execution-helpers.js +23 -7
  62. package/dist/mcp/handlers/v2-resume.d.ts +1 -1
  63. package/dist/mcp/handlers/v2-resume.js +3 -4
  64. package/dist/mcp/handlers/v2-state-conversion.js +5 -1
  65. package/dist/mcp/handlers/v2-workflow.d.ts +80 -0
  66. package/dist/mcp/handlers/v2-workflow.js +40 -23
  67. package/dist/mcp/handlers/workflow.d.ts +2 -5
  68. package/dist/mcp/handlers/workflow.js +15 -12
  69. package/dist/mcp/output-schemas.d.ts +25 -27
  70. package/dist/mcp/output-schemas.js +7 -7
  71. package/dist/mcp/server.js +23 -4
  72. package/dist/mcp/tool-call-timing.d.ts +24 -0
  73. package/dist/mcp/tool-call-timing.js +85 -0
  74. package/dist/mcp/transports/http-entry.js +3 -2
  75. package/dist/mcp/transports/http-listener.d.ts +1 -0
  76. package/dist/mcp/transports/http-listener.js +25 -0
  77. package/dist/mcp/transports/shutdown-hooks.d.ts +4 -1
  78. package/dist/mcp/transports/shutdown-hooks.js +3 -2
  79. package/dist/mcp/transports/stdio-entry.js +6 -28
  80. package/dist/mcp/v2-response-formatter.d.ts +1 -1
  81. package/dist/mcp/v2-response-formatter.js +2 -5
  82. package/dist/mcp/validation/schema-introspection.d.ts +1 -0
  83. package/dist/mcp/validation/schema-introspection.js +15 -5
  84. package/dist/mcp/validation/suggestion-generator.js +2 -2
  85. package/dist/runtime/adapters/node-process-signals.d.ts +1 -0
  86. package/dist/runtime/adapters/node-process-signals.js +5 -0
  87. package/dist/runtime/adapters/noop-process-signals.d.ts +1 -0
  88. package/dist/runtime/adapters/noop-process-signals.js +2 -0
  89. package/dist/runtime/ports/process-signals.d.ts +1 -0
  90. package/dist/types/workflow-definition.d.ts +5 -1
  91. package/dist/types/workflow-definition.js +2 -0
  92. package/dist/types/workflow.d.ts +3 -0
  93. package/dist/types/workflow.js +35 -26
  94. package/dist/v2/durable-core/domain/context-template-resolver.js +2 -2
  95. package/dist/v2/durable-core/domain/function-definition-expander.js +2 -17
  96. package/dist/v2/durable-core/domain/prompt-renderer.d.ts +2 -0
  97. package/dist/v2/durable-core/domain/prompt-renderer.js +22 -18
  98. package/dist/v2/durable-core/domain/recap-recovery.js +23 -16
  99. package/dist/v2/durable-core/domain/retrieval-contract.js +13 -7
  100. package/dist/v2/durable-core/schemas/compiled-workflow/index.js +4 -3
  101. package/dist/v2/durable-core/session-index.d.ts +22 -0
  102. package/dist/v2/durable-core/session-index.js +58 -0
  103. package/dist/v2/durable-core/sorted-event-log.d.ts +6 -0
  104. package/dist/v2/durable-core/sorted-event-log.js +15 -0
  105. package/dist/v2/infra/local/fs/index.js +8 -8
  106. package/dist/v2/infra/local/pinned-workflow-store/index.d.ts +2 -0
  107. package/dist/v2/infra/local/pinned-workflow-store/index.js +49 -0
  108. package/dist/v2/infra/local/remembered-roots-store/index.d.ts +3 -1
  109. package/dist/v2/infra/local/remembered-roots-store/index.js +6 -3
  110. package/dist/v2/infra/local/session-store/index.d.ts +1 -1
  111. package/dist/v2/infra/local/session-store/index.js +71 -61
  112. package/dist/v2/infra/local/session-summary-provider/index.js +9 -4
  113. package/dist/v2/infra/local/snapshot-store/index.js +2 -1
  114. package/dist/v2/infra/local/workspace-anchor/index.js +4 -2
  115. package/dist/v2/ports/pinned-workflow-store.port.d.ts +2 -0
  116. package/dist/v2/ports/session-event-log-store.port.d.ts +1 -1
  117. package/dist/v2/projections/assessment-consequences.d.ts +2 -1
  118. package/dist/v2/projections/assessment-consequences.js +0 -5
  119. package/dist/v2/projections/assessments.d.ts +2 -1
  120. package/dist/v2/projections/assessments.js +2 -4
  121. package/dist/v2/projections/gaps.d.ts +2 -1
  122. package/dist/v2/projections/gaps.js +0 -5
  123. package/dist/v2/projections/preferences.d.ts +2 -1
  124. package/dist/v2/projections/preferences.js +0 -5
  125. package/dist/v2/projections/run-context.d.ts +2 -2
  126. package/dist/v2/projections/run-context.js +0 -5
  127. package/dist/v2/projections/run-dag.js +7 -1
  128. package/dist/v2/projections/run-execution-trace.d.ts +8 -0
  129. package/dist/v2/projections/run-execution-trace.js +124 -0
  130. package/dist/v2/projections/run-status-signals.d.ts +2 -2
  131. package/dist/v2/usecases/console-routes.d.ts +3 -1
  132. package/dist/v2/usecases/console-routes.js +124 -25
  133. package/dist/v2/usecases/console-service.d.ts +1 -0
  134. package/dist/v2/usecases/console-service.js +83 -25
  135. package/dist/v2/usecases/console-types.d.ts +53 -0
  136. package/dist/v2/usecases/worktree-service.js +32 -1
  137. package/package.json +6 -5
  138. package/spec/workflow.schema.json +18 -0
  139. package/workflows/adaptive-ticket-creation.json +23 -16
  140. package/workflows/architecture-scalability-audit.json +29 -22
  141. package/workflows/bug-investigation.agentic.v2.json +7 -0
  142. package/workflows/coding-task-workflow-agentic.json +7 -0
  143. package/workflows/coding-task-workflow-agentic.lean.v2.json +16 -8
  144. package/workflows/coding-task-workflow-agentic.v2.json +7 -0
  145. package/workflows/cross-platform-code-conversion.v2.json +7 -0
  146. package/workflows/document-creation-workflow.json +15 -8
  147. package/workflows/documentation-update-workflow.json +15 -8
  148. package/workflows/intelligent-test-case-generation.json +7 -0
  149. package/workflows/learner-centered-course-workflow.json +9 -2
  150. package/workflows/mr-review-workflow.agentic.v2.json +7 -0
  151. package/workflows/personal-learning-materials-creation-branched.json +15 -8
  152. package/workflows/presentation-creation.json +12 -5
  153. package/workflows/production-readiness-audit.json +7 -0
  154. package/workflows/relocation-workflow-us.json +39 -32
  155. package/workflows/scoped-documentation-workflow.json +33 -26
  156. package/workflows/ui-ux-design-workflow.json +7 -0
  157. package/workflows/workflow-diagnose-environment.json +6 -0
  158. package/workflows/workflow-for-workflows.json +7 -0
  159. package/workflows/workflow-for-workflows.v2.json +23 -11
  160. package/workflows/wr.discovery.json +8 -1
  161. package/dist/console/assets/index-BZYIjrzJ.js +0 -28
  162. package/dist/console/assets/index-OLCKbDdm.css +0 -1
  163. package/dist/mcp/handlers/v2-resolve-refs-envelope.d.ts +0 -5
  164. package/dist/mcp/handlers/v2-resolve-refs-envelope.js +0 -17
@@ -8,32 +8,18 @@ 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 sseClients = new Set();
12
- let sseDebounceTimer = null;
13
- function broadcastChange() {
14
- if (sseDebounceTimer !== null)
15
- return;
16
- sseDebounceTimer = setTimeout(() => {
17
- sseDebounceTimer = null;
18
- for (const client of sseClients) {
19
- try {
20
- client.write('data: {"type":"change"}\n\n');
21
- }
22
- catch {
23
- sseClients.delete(client);
24
- }
25
- }
26
- }, 200);
27
- }
28
- function watchSessionsDir(sessionsDir) {
11
+ const dev_mode_js_1 = require("../../mcp/dev-mode.js");
12
+ function watchSessionsDir(sessionsDir, onChanged) {
29
13
  try {
30
14
  fs_1.default.mkdirSync(sessionsDir, { recursive: true });
31
15
  }
32
16
  catch { }
33
17
  let watcher = null;
34
18
  try {
35
- watcher = fs_1.default.watch(sessionsDir, { recursive: true }, () => {
36
- broadcastChange();
19
+ watcher = fs_1.default.watch(sessionsDir, { recursive: true }, (_eventType, filename) => {
20
+ if (filename !== null && filename.endsWith('.jsonl')) {
21
+ onChanged();
22
+ }
37
23
  });
38
24
  watcher.on('error', () => { });
39
25
  }
@@ -53,9 +39,38 @@ function resolveConsoleDist() {
53
39
  return legacyConsoleDist;
54
40
  return null;
55
41
  }
56
- function mountConsoleRoutes(app, consoleService) {
57
- const stopWatcher = watchSessionsDir(consoleService.getSessionsDir());
58
- process.once('exit', stopWatcher);
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);
59
74
  app.get('/api/v2/workspace/events', (req, res) => {
60
75
  res.setHeader('Content-Type', 'text/event-stream');
61
76
  res.setHeader('Cache-Control', 'no-cache');
@@ -67,12 +82,23 @@ function mountConsoleRoutes(app, consoleService) {
67
82
  req.on('close', () => { sseClients.delete(res); });
68
83
  res.on('close', () => { sseClients.delete(res); });
69
84
  });
85
+ const devMode = (0, dev_mode_js_1.isDevMode)();
86
+ if (devMode) {
87
+ app.get('/api/v2/perf/tool-calls', (req, res) => {
88
+ const rawLimit = req.query['limit'];
89
+ const limit = typeof rawLimit === 'string' ? parseInt(rawLimit, 10) : undefined;
90
+ const safeLimit = (limit !== undefined && Number.isFinite(limit) && limit > 0) ? limit : undefined;
91
+ const observations = timingRingBuffer ? timingRingBuffer.recent(safeLimit) : [];
92
+ res.json({ success: true, data: { observations, total: timingRingBuffer?.size ?? 0, devMode } });
93
+ });
94
+ }
70
95
  app.get('/api/v2/sessions', async (_req, res) => {
71
96
  const result = await consoleService.getSessionList();
72
97
  result.match((data) => res.json({ success: true, data }), (error) => res.status(500).json({ success: false, error: error.message }));
73
98
  });
74
99
  let cwdRepoRootPromise = null;
75
100
  const REPO_ROOTS_TTL_MS = 60000;
101
+ const REPO_ROOT_SESSION_STALENESS_MS = 30 * 24 * 60 * 60 * 1000;
76
102
  let cachedRepoRoots = [];
77
103
  let repoRootsExpiresAt = 0;
78
104
  app.get('/api/v2/worktrees', async (_req, res) => {
@@ -83,7 +109,11 @@ function mountConsoleRoutes(app, consoleService) {
83
109
  if (Date.now() > repoRootsExpiresAt) {
84
110
  cwdRepoRootPromise ?? (cwdRepoRootPromise = (0, worktree_service_js_1.resolveRepoRoot)(process.cwd()));
85
111
  const cwdRoot = await cwdRepoRootPromise;
86
- const rawRoots = sessions.map(s => s.repoRoot).filter((r) => r !== null);
112
+ const cutoffMs = Date.now() - REPO_ROOT_SESSION_STALENESS_MS;
113
+ const rawRoots = sessions
114
+ .filter(s => s.lastModifiedMs >= cutoffMs)
115
+ .map(s => s.repoRoot)
116
+ .filter((r) => r !== null);
87
117
  const resolvedRoots = await Promise.all(rawRoots.map(r => (0, worktree_service_js_1.resolveRepoRoot)(r)));
88
118
  const repoRootSet = new Set(resolvedRoots.filter((r) => r !== null));
89
119
  if (cwdRoot !== null)
@@ -116,10 +146,78 @@ function mountConsoleRoutes(app, consoleService) {
116
146
  res.status(status).json({ success: false, error: error.message });
117
147
  });
118
148
  });
149
+ if (workflowService) {
150
+ app.get('/api/v2/workflows', async (_req, res) => {
151
+ try {
152
+ const tagsFile = loadWorkflowTags();
153
+ const allWorkflows = await workflowService.loadAllWorkflows();
154
+ const workflows = allWorkflows
155
+ .filter((w) => !tagsFile.workflows[w.definition.id]?.hidden)
156
+ .map((w) => {
157
+ const { definition, source } = w;
158
+ const tagEntry = tagsFile.workflows[definition.id];
159
+ return {
160
+ id: definition.id,
161
+ name: definition.name,
162
+ description: definition.description,
163
+ version: definition.version,
164
+ tags: tagEntry?.tags ?? [],
165
+ source,
166
+ ...(definition.about !== undefined ? { about: definition.about } : {}),
167
+ ...(definition.examples?.length ? { examples: [...definition.examples] } : {}),
168
+ };
169
+ });
170
+ res.json({ success: true, data: { workflows } });
171
+ }
172
+ catch (e) {
173
+ res.status(500).json({ success: false, error: e instanceof Error ? e.message : String(e) });
174
+ }
175
+ });
176
+ app.get('/api/v2/workflows/:workflowId', async (req, res) => {
177
+ const { workflowId } = req.params;
178
+ try {
179
+ const workflow = await workflowService.getWorkflowById(workflowId);
180
+ if (!workflow) {
181
+ return res.status(404).json({ success: false, error: `Workflow not found: ${workflowId}` });
182
+ }
183
+ const tagsFile = loadWorkflowTags();
184
+ if (tagsFile.workflows[workflowId]?.hidden) {
185
+ return res.status(404).json({ success: false, error: `Workflow not found: ${workflowId}` });
186
+ }
187
+ const { definition, source } = workflow;
188
+ const tagEntry = tagsFile.workflows[workflowId];
189
+ return res.json({
190
+ success: true,
191
+ data: {
192
+ id: definition.id,
193
+ name: definition.name,
194
+ description: definition.description,
195
+ version: definition.version,
196
+ tags: tagEntry?.tags ?? [],
197
+ source,
198
+ stepCount: definition.steps.length,
199
+ ...(definition.about !== undefined ? { about: definition.about } : {}),
200
+ ...(definition.examples?.length ? { examples: [...definition.examples] } : {}),
201
+ ...(definition.preconditions?.length ? { preconditions: [...definition.preconditions] } : {}),
202
+ },
203
+ });
204
+ }
205
+ catch (e) {
206
+ return res.status(500).json({ success: false, error: e instanceof Error ? e.message : String(e) });
207
+ }
208
+ });
209
+ }
119
210
  const consoleDist = resolveConsoleDist();
120
211
  if (consoleDist) {
121
- app.use('/console', express_1.default.static(consoleDist));
212
+ app.use('/console', express_1.default.static(consoleDist, {
213
+ setHeaders(res, filePath) {
214
+ if (path_1.default.basename(filePath) === 'index.html') {
215
+ res.setHeader('Cache-Control', 'no-cache');
216
+ }
217
+ }
218
+ }));
122
219
  app.get('/console/*path', (_req, res) => {
220
+ res.setHeader('Cache-Control', 'no-cache');
123
221
  res.sendFile(path_1.default.join(consoleDist, 'index.html'));
124
222
  });
125
223
  console.error(`[Console] UI serving from ${consoleDist}`);
@@ -133,4 +231,5 @@ function mountConsoleRoutes(app, consoleService) {
133
231
  });
134
232
  console.error('[Console] UI not found (run: cd console && npm run build)');
135
233
  }
234
+ return stopWatcher;
136
235
  }
@@ -15,6 +15,7 @@ export interface ConsoleServicePorts {
15
15
  export declare class ConsoleService {
16
16
  private readonly ports;
17
17
  constructor(ports: ConsoleServicePorts);
18
+ private readonly _summaryCache;
18
19
  getSessionsDir(): string;
19
20
  getSessionList(): ResultAsync<ConsoleSessionListResponse, ConsoleServiceError>;
20
21
  getSessionDetail(sessionIdStr: string): ResultAsync<ConsoleSessionDetail, 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,7 @@ 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();
21
24
  }
22
25
  getSessionsDir() {
23
26
  return this.ports.dataDir.sessionsDir();
@@ -103,6 +106,10 @@ class ConsoleService {
103
106
  });
104
107
  }
105
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
+ }
106
113
  return this.ports.sessionStore
107
114
  .load(sessionId)
108
115
  .andThen((truth) => {
@@ -110,10 +117,22 @@ class ConsoleService {
110
117
  const workflowNamesRA = dagRes.isOk()
111
118
  ? resolveWorkflowNames(dagRes.value, this.ports.pinnedWorkflowStore)
112
119
  : (0, neverthrow_2.okAsync)({});
120
+ const completionRA = dagRes.isOk()
121
+ ? resolveRunCompletionFromDag(dagRes.value, this.ports.snapshotStore)
122
+ : (0, neverthrow_2.okAsync)({});
113
123
  return neverthrow_1.ResultAsync.combine([
114
- resolveRunCompletion(truth.events, this.ports.snapshotStore),
124
+ completionRA,
115
125
  workflowNamesRA,
116
- ]).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;
117
136
  })
118
137
  .orElse(() => (0, neverthrow_2.okAsync)(null));
119
138
  }
@@ -239,8 +258,8 @@ function extractStepTitlesFromCompiled(compiled) {
239
258
  return titles;
240
259
  }
241
260
  const TITLE_CONTEXT_KEYS = ['goal', 'taskDescription', 'mrTitle', 'prTitle', 'ticketTitle', 'problem'];
242
- function deriveSessionTitle(events) {
243
- const contextRes = (0, run_context_js_1.projectRunContextV2)(events);
261
+ function deriveSessionTitle(sortedEvents) {
262
+ const contextRes = (0, run_context_js_1.projectRunContextV2)(sortedEvents);
244
263
  if (contextRes.isOk()) {
245
264
  for (const runCtx of Object.values(contextRes.value.byRunId)) {
246
265
  for (const key of TITLE_CONTEXT_KEYS) {
@@ -251,7 +270,7 @@ function deriveSessionTitle(events) {
251
270
  }
252
271
  }
253
272
  }
254
- const title = extractTitleFromFirstRecap(events);
273
+ const title = extractTitleFromFirstRecap(sortedEvents);
255
274
  if (title)
256
275
  return title;
257
276
  return null;
@@ -333,19 +352,26 @@ function truncateTitle(text, maxLen = 120) {
333
352
  return text;
334
353
  return text.slice(0, maxLen - 1) + '…';
335
354
  }
336
- function projectSessionSummary(sessionId, truth, completionByRunId, workflowNames, lastModifiedMs, nowMs) {
355
+ function projectSessionSummary(sessionId, truth, completionByRunId, workflowNames, lastModifiedMs, nowMs, precomputedDag) {
337
356
  const { events } = truth;
338
357
  const health = (0, session_health_js_1.projectSessionHealthV2)(truth);
339
358
  if (health.isErr())
340
359
  return null;
341
360
  const sessionHealth = health.value.kind === 'healthy' ? 'healthy' : 'corrupt';
342
- const dagRes = (0, run_dag_js_1.projectRunDagV2)(events);
343
- 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)
344
370
  return null;
345
- const dag = dagRes.value;
346
- const statusRes = (0, run_status_signals_js_1.projectRunStatusSignalsV2)(events);
347
- const gapsRes = (0, gaps_js_1.projectGapsV2)(events);
348
- 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;
349
375
  const gitBranch = extractGitBranch(events);
350
376
  const repoRoot = extractRepoRoot(events);
351
377
  const runs = Object.values(dag.runsById);
@@ -418,26 +444,52 @@ function projectSessionDetail(sessionId, truth, completionByRunId, stepLabels, w
418
444
  const { events } = truth;
419
445
  const health = (0, session_health_js_1.projectSessionHealthV2)(truth);
420
446
  const sessionHealth = health.isOk() && health.value.kind === 'healthy' ? 'healthy' : 'corrupt';
421
- const sessionTitle = deriveSessionTitle(events);
447
+ const sortedEventsRes = (0, sorted_event_log_js_1.asSortedEventLog)(events);
448
+ const sessionTitle = sortedEventsRes.isOk() ? deriveSessionTitle(sortedEventsRes.value) : null;
422
449
  const dagRes = (0, run_dag_js_1.projectRunDagV2)(events);
423
450
  if (dagRes.isErr()) {
424
451
  return { sessionId, sessionTitle, health: sessionHealth, runs: [] };
425
452
  }
426
- const statusRes = (0, run_status_signals_js_1.projectRunStatusSignalsV2)(events);
427
- 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
+ }
428
472
  const runs = Object.values(dagRes.value.runsById).map((run) => {
429
473
  const statusSignals = statusRes.isOk() ? statusRes.value.byRunId[run.runId] : undefined;
430
474
  const status = deriveRunStatus(statusSignals?.isBlocked ?? false, statusSignals?.hasUnresolvedCriticalGaps ?? false, completionByRunId[run.runId] ?? false);
431
475
  const tipSet = new Set(run.tipNodeIds);
432
- const nodes = Object.values(run.nodesById).map((node) => ({
433
- nodeId: node.nodeId,
434
- nodeKind: node.nodeKind,
435
- parentNodeId: node.parentNodeId,
436
- createdAtEventIndex: node.createdAtEventIndex,
437
- isPreferredTip: node.nodeId === run.preferredTipNodeId,
438
- isTip: tipSet.has(node.nodeId),
439
- stepLabel: stepLabels[node.nodeId] ?? null,
440
- }));
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
+ });
441
493
  const edges = run.edges.map((edge) => ({
442
494
  edgeKind: edge.edgeKind,
443
495
  fromNodeId: edge.fromNodeId,
@@ -459,6 +511,9 @@ function projectSessionDetail(sessionId, truth, completionByRunId, stepLabels, w
459
511
  hasUnresolvedCriticalGaps: gapsRes.isOk()
460
512
  ? (gapsRes.value.unresolvedCriticalByRunId[run.runId]?.length ?? 0) > 0
461
513
  : false,
514
+ executionTraceSummary: executionTraceRes.isOk()
515
+ ? (executionTraceRes.value.byRunId[run.runId] ?? null)
516
+ : null,
462
517
  };
463
518
  });
464
519
  return { sessionId, sessionTitle, health: sessionHealth, runs };
@@ -566,7 +621,10 @@ function extractValidations(events, nodeId) {
566
621
  return results;
567
622
  }
568
623
  function extractGaps(events, nodeId) {
569
- 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);
570
628
  if (gapsRes.isErr())
571
629
  return [];
572
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;
@@ -128,3 +152,32 @@ export interface ConsoleNodeDetail {
128
152
  readonly validations: readonly ConsoleValidationResult[];
129
153
  readonly gaps: readonly ConsoleNodeGap[];
130
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,6 +44,29 @@ 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
+ }
47
70
  function parseFileStatus(xy) {
48
71
  if (xy === '??')
49
72
  return 'untracked';
@@ -113,7 +136,15 @@ async function enrichRepo(repoRoot, activeSessions) {
113
136
  if (porcelain === null)
114
137
  return null;
115
138
  const rawWorktrees = parseWorktreePorcelain(porcelain);
116
- 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
+ }));
117
148
  const worktrees = rawWorktrees.flatMap((wt, i) => {
118
149
  const result = results[i];
119
150
  if (result.status === 'rejected') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exaudeus/workrail",
3
- "version": "3.15.0",
3
+ "version": "3.17.0",
4
4
  "description": "Step-by-step workflow enforcement for AI agents via MCP",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -77,7 +77,8 @@
77
77
  "codemod:v2-contexts": "npx ts-node scripts/codemods/run.ts --mod v2-contexts --tsconfig tsconfig.test.json --write",
78
78
  "codemod:v2-prune": "npx ts-node scripts/codemods/run.ts --mod v2-prune --tsconfig tsconfig.test.json --write",
79
79
  "codemod:guard": "npx ts-node scripts/codemods/run.ts --mod guard --tsconfig tsconfig.test.json",
80
- "codemod:test-platform-guard": "npx ts-node scripts/codemods/run.ts --mod test-platform-guard --tsconfig tsconfig.test.json"
80
+ "codemod:test-platform-guard": "npx ts-node scripts/codemods/run.ts --mod test-platform-guard --tsconfig tsconfig.test.json",
81
+ "prepare": "bash scripts/setup-hooks.sh"
81
82
  },
82
83
  "dependencies": {
83
84
  "@modelcontextprotocol/sdk": "^1.24.0",
@@ -89,7 +90,7 @@
89
90
  "dotenv": "^17.2.0",
90
91
  "express": "^5.1.0",
91
92
  "neverthrow": "^8.2.0",
92
- "open": "^10.2.0",
93
+ "open": "^11.0.0",
93
94
  "reflect-metadata": "^0.2.0",
94
95
  "semver": "^7.7.2",
95
96
  "tsconfig-paths": "^4.2.0",
@@ -121,8 +122,8 @@
121
122
  "happy-dom": "^20.0.11",
122
123
  "jsdom": "^27.0.0",
123
124
  "lit": "^3.3.1",
124
- "node-fetch": "^2.7.0",
125
- "semantic-release": "^24.2.0",
125
+ "node-fetch": "^3.3.2",
126
+ "semantic-release": "^25.0.3",
126
127
  "ts-morph": "^27.0.2",
127
128
  "typescript": "^5.9.3",
128
129
  "vite": "^7.1.9",
@@ -182,6 +182,24 @@
182
182
  "type": "integer",
183
183
  "minimum": 1,
184
184
  "description": "The authoring spec version this workflow was last validated against."
185
+ },
186
+ "about": {
187
+ "type": "string",
188
+ "description": "Human-readable overview for display in the console and other UIs. Markdown is supported. Write for a user deciding whether to use this workflow: what it does, when to use it, what it produces, and how to get good results. User-facing surface -- not an agent instruction (use metaGuidance for that).",
189
+ "minLength": 1,
190
+ "maxLength": 4096
191
+ },
192
+ "examples": {
193
+ "type": "array",
194
+ "description": "Short illustrative goal strings showing what this workflow is used for. Write for humans browsing the catalog and for agents selecting the right workflow. Each item should be concrete and specific enough to be informative.",
195
+ "items": {
196
+ "type": "string",
197
+ "minLength": 10,
198
+ "maxLength": 120
199
+ },
200
+ "minItems": 1,
201
+ "maxItems": 6,
202
+ "uniqueItems": true
185
203
  }
186
204
  },
187
205
  "required": [