@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
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>WorkRail Console</title>
7
- <script type="module" crossorigin src="/console/assets/index-BZYIjrzJ.js"></script>
8
- <link rel="stylesheet" crossorigin href="/console/assets/index-OLCKbDdm.css">
7
+ <script type="module" crossorigin src="/console/assets/index-BwJelCXK.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/console/assets/index-BZNM03t1.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="root"></div>
@@ -3,6 +3,7 @@ import { container } from 'tsyringe';
3
3
  import type { RuntimeMode } from '../runtime/runtime-mode.js';
4
4
  export interface ContainerInitOptions {
5
5
  readonly runtimeMode?: RuntimeMode;
6
+ readonly env?: Record<string, string | undefined>;
6
7
  }
7
8
  export declare function initializeContainer(options?: ContainerInitOptions): Promise<void>;
8
9
  export declare function startAsyncServices(): Promise<void>;
@@ -50,14 +50,28 @@ const in_memory_shutdown_events_js_1 = require("../runtime/adapters/in-memory-sh
50
50
  const node_process_terminator_js_1 = require("../runtime/adapters/node-process-terminator.js");
51
51
  const throwing_process_terminator_js_1 = require("../runtime/adapters/throwing-process-terminator.js");
52
52
  const app_config_js_1 = require("../config/app-config.js");
53
+ const config_file_js_1 = require("../config/config-file.js");
53
54
  const formatter_js_1 = require("../errors/formatter.js");
54
55
  let initialized = false;
55
56
  let asyncInitialized = false;
56
57
  let initializationPromise = null;
57
58
  let isInitializing = false;
58
- async function registerConfig() {
59
+ let mergedEnv = process.env;
60
+ async function registerConfig(env) {
61
+ (0, config_file_js_1.ensureWorkrailConfigFile)();
62
+ if (env !== undefined) {
63
+ mergedEnv = env;
64
+ }
65
+ else if (process.env['VITEST']) {
66
+ mergedEnv = { ...process.env };
67
+ }
68
+ else {
69
+ const configFileResult = (0, config_file_js_1.loadWorkrailConfigFile)();
70
+ const configFileValues = configFileResult.kind === 'ok' ? configFileResult.value : {};
71
+ mergedEnv = { ...configFileValues, ...process.env };
72
+ }
59
73
  if (!tsyringe_1.container.isRegistered(tokens_js_1.DI.Config.App)) {
60
- const configResult = (0, app_config_js_1.loadConfig)({ env: process.env, projectPath: process.cwd() });
74
+ const configResult = (0, app_config_js_1.loadConfig)({ env: mergedEnv, projectPath: process.cwd() });
61
75
  if (configResult.kind === 'err') {
62
76
  console.error((0, formatter_js_1.formatAppError)(configResult.error));
63
77
  process.exit(1);
@@ -71,9 +85,10 @@ async function registerConfig() {
71
85
  tsyringe_1.container.register(tokens_js_1.DI.Config.BrowserBehavior, { useValue: config.dashboard.browserBehavior });
72
86
  }
73
87
  if (!tsyringe_1.container.isRegistered(tokens_js_1.DI.Infra.FeatureFlags)) {
74
- const { EnvironmentFeatureFlagProvider } = await Promise.resolve().then(() => __importStar(require('../config/feature-flags.js')));
88
+ const { CustomEnvFeatureFlagProvider } = await Promise.resolve().then(() => __importStar(require('../config/feature-flags.js')));
89
+ const captured = mergedEnv;
75
90
  tsyringe_1.container.register(tokens_js_1.DI.Infra.FeatureFlags, {
76
- useFactory: (0, tsyringe_1.instanceCachingFactory)((c) => c.resolve(EnvironmentFeatureFlagProvider))
91
+ useFactory: (0, tsyringe_1.instanceCachingFactory)(() => new CustomEnvFeatureFlagProvider(captured))
77
92
  });
78
93
  }
79
94
  }
@@ -186,7 +201,7 @@ async function registerV2Services() {
186
201
  const { NodeTimeClockV2 } = await Promise.resolve().then(() => __importStar(require('../v2/infra/local/time-clock/index.js')));
187
202
  const { IdFactoryV2 } = await Promise.resolve().then(() => __importStar(require('../v2/infra/local/id-factory/index.js')));
188
203
  tsyringe_1.container.register(tokens_js_1.DI.V2.DataDir, {
189
- useFactory: (0, tsyringe_1.instanceCachingFactory)(() => new LocalDataDirV2(process.env)),
204
+ useFactory: (0, tsyringe_1.instanceCachingFactory)(() => new LocalDataDirV2(mergedEnv)),
190
205
  });
191
206
  tsyringe_1.container.register(tokens_js_1.DI.V2.FileSystem, {
192
207
  useFactory: (0, tsyringe_1.instanceCachingFactory)(() => new NodeFileSystemV2()),
@@ -249,7 +264,8 @@ async function registerV2Services() {
249
264
  useFactory: (0, tsyringe_1.instanceCachingFactory)((c) => {
250
265
  const dataDir = c.resolve(tokens_js_1.DI.V2.DataDir);
251
266
  const fs = c.resolve(tokens_js_1.DI.V2.FileSystem);
252
- return new LocalRememberedRootsStoreV2(dataDir, fs);
267
+ const clock = c.resolve(tokens_js_1.DI.V2.TimeClock);
268
+ return new LocalRememberedRootsStoreV2(dataDir, fs, clock);
253
269
  }),
254
270
  });
255
271
  tsyringe_1.container.register(tokens_js_1.DI.V2.ManagedSourceStore, {
@@ -321,7 +337,7 @@ async function initializeContainer(options = {}) {
321
337
  isInitializing = true;
322
338
  try {
323
339
  registerRuntime(options);
324
- await registerConfig();
340
+ await registerConfig(options.env);
325
341
  await registerStorageChain();
326
342
  await registerV2Services();
327
343
  await registerServices();
@@ -366,6 +382,7 @@ function resetContainer() {
366
382
  asyncInitialized = false;
367
383
  initializationPromise = null;
368
384
  isInitializing = false;
385
+ mergedEnv = process.env;
369
386
  }
370
387
  function isInitialized() {
371
388
  return initialized;
@@ -42,18 +42,17 @@ export declare class HttpServer {
42
42
  private tryBecomePrimary;
43
43
  private shouldReclaimLock;
44
44
  private reclaimStaleLock;
45
- private checkHealth;
46
45
  private setupPrimaryCleanup;
47
46
  private startAsPrimary;
48
47
  private startLegacyMode;
49
48
  private printBanner;
50
49
  openDashboard(sessionId?: string): Promise<string>;
51
50
  stop(): Promise<void>;
52
- mountRoutes(installer: (app: Application) => void): void;
51
+ mountRoutes(installer: (app: Application) => (() => void) | void): void;
52
+ private readonly _routeDisposers;
53
53
  finalize(): void;
54
54
  getBaseUrl(): string;
55
55
  getPort(): number;
56
- private quickCleanup;
57
- private getWorkrailPorts;
58
56
  fullCleanup(): Promise<number>;
57
+ private getWorkrailPorts;
59
58
  }
@@ -29,6 +29,8 @@ const DashboardLockRelease_js_1 = require("./DashboardLockRelease.js");
29
29
  const cors_1 = __importDefault(require("cors"));
30
30
  const open_1 = __importDefault(require("open"));
31
31
  const child_process_1 = require("child_process");
32
+ const _pkg = require(path_1.default.join(__dirname, '../../../package.json'));
33
+ const CURRENT_VERSION = _pkg.version;
32
34
  let HttpServer = class HttpServer {
33
35
  constructor(sessionManager, processLifecyclePolicy, processSignals, shutdownEvents, dashboardMode, browserBehavior) {
34
36
  this.sessionManager = sessionManager;
@@ -41,6 +43,7 @@ let HttpServer = class HttpServer {
41
43
  this.baseUrl = '';
42
44
  this.isPrimary = false;
43
45
  this.config = {};
46
+ this._routeDisposers = [];
44
47
  this.port = 3456;
45
48
  this.lockFile = path_1.default.join(os_1.default.homedir(), '.workrail', 'dashboard.lock');
46
49
  this.heartbeat = new DashboardHeartbeat_js_1.DashboardHeartbeat(this.lockFile, () => this.isPrimary);
@@ -314,7 +317,7 @@ let HttpServer = class HttpServer {
314
317
  });
315
318
  }
316
319
  });
317
- this.app.get('/api/health', (req, res) => {
320
+ this.app.get('/api/health', (_req, res) => {
318
321
  res.json({
319
322
  success: true,
320
323
  status: 'healthy',
@@ -322,12 +325,12 @@ let HttpServer = class HttpServer {
322
325
  timestamp: new Date().toISOString(),
323
326
  isPrimary: this.isPrimary,
324
327
  pid: process.pid,
325
- port: this.port
328
+ port: this.port,
329
+ version: CURRENT_VERSION
326
330
  });
327
331
  });
328
332
  }
329
333
  async start() {
330
- await this.quickCleanup();
331
334
  const mode = this.config.dashboardMode ?? this.dashboardMode;
332
335
  if (mode.kind === 'legacy') {
333
336
  console.error('[Dashboard] Unified dashboard disabled, using legacy mode');
@@ -340,7 +343,8 @@ let HttpServer = class HttpServer {
340
343
  }
341
344
  catch (error) {
342
345
  if (error.code === 'EADDRINUSE') {
343
- console.error(`[Dashboard] Port ${this.port} busy despite lock, falling back to legacy mode`);
346
+ console.error(`[Dashboard] Port ${this.port} still held by previous instance -- ` +
347
+ `running on next available port. Restart the old instance to move to ${this.port}.`);
344
348
  await promises_1.default.unlink(this.lockFile).catch(() => { });
345
349
  return await this.startLegacyMode();
346
350
  }
@@ -361,7 +365,8 @@ let HttpServer = class HttpServer {
361
365
  startedAt: new Date().toISOString(),
362
366
  lastHeartbeat: new Date().toISOString(),
363
367
  projectId: this.sessionManager.getProjectId(),
364
- projectPath: this.sessionManager.getProjectPath()
368
+ projectPath: this.sessionManager.getProjectPath(),
369
+ version: CURRENT_VERSION
365
370
  };
366
371
  await promises_1.default.writeFile(this.lockFile, JSON.stringify(lockData, null, 2), { flag: 'wx' });
367
372
  console.error('[Dashboard] Primary elected');
@@ -385,6 +390,9 @@ let HttpServer = class HttpServer {
385
390
  if (!lockData.pid || !lockData.port || !lockData.startedAt) {
386
391
  return { reclaim: true, reason: 'invalid lock structure' };
387
392
  }
393
+ if (lockData.version !== CURRENT_VERSION) {
394
+ return { reclaim: true, reason: `version mismatch (lock=${lockData.version}, current=${CURRENT_VERSION})` };
395
+ }
388
396
  const lastHeartbeat = new Date(lockData.lastHeartbeat || lockData.startedAt);
389
397
  const ageMinutes = (Date.now() - lastHeartbeat.getTime()) / 60000;
390
398
  if (ageMinutes > 2) {
@@ -404,20 +412,8 @@ let HttpServer = class HttpServer {
404
412
  const lockData = JSON.parse(lockContent);
405
413
  const { reclaim, reason } = this.shouldReclaimLock(lockData);
406
414
  if (!reclaim) {
407
- const isHealthy = await this.checkHealth(lockData.port);
408
- if (isHealthy) {
409
- return false;
410
- }
411
- console.error(`[Dashboard] Primary (PID ${lockData.pid}) not responding, attempting graceful shutdown`);
412
- try {
413
- process.kill(lockData.pid, 'SIGTERM');
414
- await new Promise(resolve => setTimeout(resolve, 2000));
415
- }
416
- catch { }
417
- const stillHealthy = await this.checkHealth(lockData.port);
418
- if (stillHealthy) {
419
- return false;
420
- }
415
+ console.error(`[Dashboard] Secondary mode: primary lock valid (PID ${lockData.pid}), yielding`);
416
+ return false;
421
417
  }
422
418
  else {
423
419
  console.error(`[Dashboard] Lock reclaim needed: ${reason}`);
@@ -429,7 +425,8 @@ let HttpServer = class HttpServer {
429
425
  startedAt: new Date().toISOString(),
430
426
  lastHeartbeat: new Date().toISOString(),
431
427
  projectId: this.sessionManager.getProjectId(),
432
- projectPath: this.sessionManager.getProjectPath()
428
+ projectPath: this.sessionManager.getProjectPath(),
429
+ version: CURRENT_VERSION
433
430
  };
434
431
  try {
435
432
  await promises_1.default.writeFile(tempPath, JSON.stringify(newLockData, null, 2));
@@ -474,23 +471,6 @@ let HttpServer = class HttpServer {
474
471
  return await this.tryBecomePrimary();
475
472
  }
476
473
  }
477
- async checkHealth(port) {
478
- try {
479
- const controller = new AbortController();
480
- const timeout = setTimeout(() => controller.abort(), 2000);
481
- const response = await fetch(`http://localhost:${port}/api/health`, {
482
- signal: controller.signal
483
- });
484
- clearTimeout(timeout);
485
- if (!response.ok)
486
- return false;
487
- const data = await response.json();
488
- return data.status === 'healthy' && data.isPrimary !== undefined;
489
- }
490
- catch {
491
- return false;
492
- }
493
- }
494
474
  setupPrimaryCleanup() {
495
475
  if (this.processLifecyclePolicy.kind === 'no_signal_handlers') {
496
476
  return;
@@ -609,6 +589,13 @@ let HttpServer = class HttpServer {
609
589
  async stop() {
610
590
  this.heartbeat.stop();
611
591
  this.sessionManager.unwatchAll();
592
+ for (const dispose of this._routeDisposers) {
593
+ try {
594
+ dispose();
595
+ }
596
+ catch { }
597
+ }
598
+ this._routeDisposers.length = 0;
612
599
  await new Promise((resolve) => {
613
600
  if (!this.server)
614
601
  return resolve();
@@ -628,7 +615,10 @@ let HttpServer = class HttpServer {
628
615
  }
629
616
  }
630
617
  mountRoutes(installer) {
631
- installer(this.app);
618
+ const disposer = installer(this.app);
619
+ if (disposer != null) {
620
+ this._routeDisposers.push(disposer);
621
+ }
632
622
  }
633
623
  finalize() {
634
624
  this.app.use((req, res) => {
@@ -645,42 +635,50 @@ let HttpServer = class HttpServer {
645
635
  getPort() {
646
636
  return this.port;
647
637
  }
648
- async quickCleanup() {
638
+ async fullCleanup() {
649
639
  try {
650
640
  const busyPorts = await this.getWorkrailPorts();
651
641
  if (busyPorts.length === 0) {
652
- return;
642
+ console.error('[Cleanup] No workrail processes found');
643
+ return 0;
653
644
  }
654
- console.error(`[Cleanup] Found ${busyPorts.length} workrail process(es), checking health...`);
645
+ console.error(`[Cleanup] Found ${busyPorts.length} workrail process(es), removing all...`);
655
646
  let cleanedCount = 0;
656
647
  for (const { port, pid } of busyPorts) {
657
- if (pid === process.pid)
648
+ if (pid === process.pid) {
649
+ console.error(`[Cleanup] Skipping current process ${pid}`);
658
650
  continue;
659
- const isHealthy = await this.checkHealth(port);
660
- if (!isHealthy) {
661
- console.error(`[Cleanup] Removing unresponsive process ${pid} on port ${port}`);
651
+ }
652
+ console.error(`[Cleanup] Killing process ${pid} on port ${port}`);
653
+ try {
654
+ process.kill(pid, 'SIGTERM');
655
+ await new Promise(r => setTimeout(r, 1000));
662
656
  try {
663
- process.kill(pid, 'SIGTERM');
664
- await new Promise(r => setTimeout(r, 1000));
665
- try {
666
- process.kill(pid, 0);
667
- process.kill(pid, 'SIGKILL');
668
- }
669
- catch {
670
- }
671
- cleanedCount++;
657
+ process.kill(pid, 0);
658
+ process.kill(pid, 'SIGKILL');
659
+ console.error(`[Cleanup] Force killed process ${pid}`);
672
660
  }
673
- catch (error) {
661
+ catch {
662
+ console.error(`[Cleanup] Process ${pid} terminated gracefully`);
674
663
  }
664
+ cleanedCount++;
665
+ }
666
+ catch (error) {
667
+ console.error(`[Cleanup] Failed to kill process ${pid}:`, error);
675
668
  }
676
669
  }
677
- if (cleanedCount > 0) {
678
- console.error(`[Cleanup] Cleaned up ${cleanedCount} orphaned process(es)`);
679
- await new Promise(r => setTimeout(r, 1000));
670
+ console.error(`[Cleanup] Cleaned up ${cleanedCount} process(es)`);
671
+ try {
672
+ await promises_1.default.unlink(this.lockFile);
673
+ console.error('[Cleanup] Removed lock file');
674
+ }
675
+ catch {
680
676
  }
677
+ return cleanedCount;
681
678
  }
682
679
  catch (error) {
683
- console.error('[Cleanup] Failed, continuing anyway:', error);
680
+ console.error('[Cleanup] Full cleanup failed:', error);
681
+ throw error;
684
682
  }
685
683
  }
686
684
  async getWorkrailPorts() {
@@ -719,56 +717,10 @@ let HttpServer = class HttpServer {
719
717
  }
720
718
  return [];
721
719
  }
722
- catch (error) {
720
+ catch {
723
721
  return [];
724
722
  }
725
723
  }
726
- async fullCleanup() {
727
- try {
728
- const busyPorts = await this.getWorkrailPorts();
729
- if (busyPorts.length === 0) {
730
- console.error('[Cleanup] No workrail processes found');
731
- return 0;
732
- }
733
- console.error(`[Cleanup] Found ${busyPorts.length} workrail process(es), removing all...`);
734
- let cleanedCount = 0;
735
- for (const { port, pid } of busyPorts) {
736
- if (pid === process.pid) {
737
- console.error(`[Cleanup] Skipping current process ${pid}`);
738
- continue;
739
- }
740
- console.error(`[Cleanup] Killing process ${pid} on port ${port}`);
741
- try {
742
- process.kill(pid, 'SIGTERM');
743
- await new Promise(r => setTimeout(r, 1000));
744
- try {
745
- process.kill(pid, 0);
746
- process.kill(pid, 'SIGKILL');
747
- console.error(`[Cleanup] Force killed process ${pid}`);
748
- }
749
- catch {
750
- console.error(`[Cleanup] Process ${pid} terminated gracefully`);
751
- }
752
- cleanedCount++;
753
- }
754
- catch (error) {
755
- console.error(`[Cleanup] Failed to kill process ${pid}:`, error);
756
- }
757
- }
758
- console.error(`[Cleanup] Cleaned up ${cleanedCount} process(es)`);
759
- try {
760
- await promises_1.default.unlink(this.lockFile);
761
- console.error('[Cleanup] Removed lock file');
762
- }
763
- catch {
764
- }
765
- return cleanedCount;
766
- }
767
- catch (error) {
768
- console.error('[Cleanup] Full cleanup failed:', error);
769
- throw error;
770
- }
771
- }
772
724
  };
773
725
  exports.HttpServer = HttpServer;
774
726
  exports.HttpServer = HttpServer = __decorate([
@@ -6,6 +6,7 @@ export declare class CachingWorkflowStorage implements IWorkflowStorage {
6
6
  readonly kind: "single";
7
7
  private workflowCache;
8
8
  private summaryCache;
9
+ private workflowCacheIndex;
9
10
  private stats;
10
11
  constructor(inner: IWorkflowStorage, ttlMs: number);
11
12
  get source(): WorkflowSource;
@@ -26,6 +27,7 @@ export declare class CachingCompositeWorkflowStorage implements ICompositeWorkfl
26
27
  readonly kind: "composite";
27
28
  private workflowCache;
28
29
  private summaryCache;
30
+ private workflowCacheIndex;
29
31
  private stats;
30
32
  constructor(inner: ICompositeWorkflowStorage, ttlMs: number);
31
33
  private isFresh;
@@ -14,6 +14,7 @@ class CachingWorkflowStorage {
14
14
  this.kind = 'single';
15
15
  this.workflowCache = null;
16
16
  this.summaryCache = null;
17
+ this.workflowCacheIndex = null;
17
18
  this.stats = { hits: 0, misses: 0 };
18
19
  }
19
20
  get source() {
@@ -25,6 +26,7 @@ class CachingWorkflowStorage {
25
26
  clearCache() {
26
27
  this.workflowCache = null;
27
28
  this.summaryCache = null;
29
+ this.workflowCacheIndex = null;
28
30
  }
29
31
  isFresh(cache) {
30
32
  return cache !== null && Date.now() - cache.timestamp < this.ttlMs;
@@ -37,16 +39,18 @@ class CachingWorkflowStorage {
37
39
  this.stats.misses += 1;
38
40
  const workflows = await this.inner.loadAllWorkflows();
39
41
  this.workflowCache = { value: workflows, timestamp: Date.now() };
42
+ this.workflowCacheIndex = new Map(workflows.map((w) => [w.definition.id, w]));
40
43
  this.summaryCache = null;
41
44
  return deepClone(workflows);
42
45
  }
43
46
  async getWorkflowById(id) {
44
- if (this.isFresh(this.workflowCache)) {
45
- const wf = this.workflowCache.value.find((w) => w.definition.id === id);
46
- if (wf) {
47
+ if (this.isFresh(this.workflowCache) && this.workflowCacheIndex !== null) {
48
+ const wf = this.workflowCacheIndex.get(id);
49
+ if (wf !== undefined) {
47
50
  this.stats.hits += 1;
48
51
  return deepClone(wf);
49
52
  }
53
+ return null;
50
54
  }
51
55
  this.stats.misses += 1;
52
56
  const workflow = await this.inner.getWorkflowById(id);
@@ -77,6 +81,7 @@ class CachingCompositeWorkflowStorage {
77
81
  this.kind = 'composite';
78
82
  this.workflowCache = null;
79
83
  this.summaryCache = null;
84
+ this.workflowCacheIndex = null;
80
85
  this.stats = { hits: 0, misses: 0 };
81
86
  }
82
87
  isFresh(cache) {
@@ -96,16 +101,18 @@ class CachingCompositeWorkflowStorage {
96
101
  this.stats.misses += 1;
97
102
  const workflows = await this.inner.loadAllWorkflows();
98
103
  this.workflowCache = { value: workflows, timestamp: Date.now() };
104
+ this.workflowCacheIndex = new Map(workflows.map((w) => [w.definition.id, w]));
99
105
  this.summaryCache = null;
100
106
  return deepClone(workflows);
101
107
  }
102
108
  async getWorkflowById(id) {
103
- if (this.isFresh(this.workflowCache)) {
104
- const wf = this.workflowCache.value.find((w) => w.definition.id === id);
105
- if (wf) {
109
+ if (this.isFresh(this.workflowCache) && this.workflowCacheIndex !== null) {
110
+ const wf = this.workflowCacheIndex.get(id);
111
+ if (wf !== undefined) {
106
112
  this.stats.hits += 1;
107
113
  return deepClone(wf);
108
114
  }
115
+ return null;
109
116
  }
110
117
  this.stats.misses += 1;
111
118
  const workflow = await this.inner.getWorkflowById(id);
@@ -126,6 +133,7 @@ class CachingCompositeWorkflowStorage {
126
133
  await this.inner.save(definition);
127
134
  this.workflowCache = null;
128
135
  this.summaryCache = null;
136
+ this.workflowCacheIndex = null;
129
137
  }
130
138
  }
131
139
  getCacheStats() {
@@ -134,6 +142,7 @@ class CachingCompositeWorkflowStorage {
134
142
  clearCache() {
135
143
  this.workflowCache = null;
136
144
  this.summaryCache = null;
145
+ this.workflowCacheIndex = null;
137
146
  }
138
147
  }
139
148
  exports.CachingCompositeWorkflowStorage = CachingCompositeWorkflowStorage;
@@ -6,7 +6,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.FileWorkflowStorage = void 0;
7
7
  exports.createDefaultFileWorkflowStorage = createDefaultFileWorkflowStorage;
8
8
  const promises_1 = __importDefault(require("fs/promises"));
9
- const fs_1 = require("fs");
10
9
  const path_1 = __importDefault(require("path"));
11
10
  const workflow_1 = require("../../types/workflow");
12
11
  const error_handler_1 = require("../../core/error-handler");
@@ -63,7 +62,7 @@ class FileWorkflowStorage {
63
62
  }
64
63
  const filePathRaw = path_1.default.resolve(this.baseDirReal, file);
65
64
  assertWithinBase(filePathRaw, this.baseDirReal);
66
- const stats = (0, fs_1.statSync)(filePathRaw);
65
+ const stats = await promises_1.default.stat(filePathRaw);
67
66
  if (stats.size > this.maxFileSize)
68
67
  continue;
69
68
  const raw = await promises_1.default.readFile(filePathRaw, 'utf-8');
@@ -94,7 +93,7 @@ class FileWorkflowStorage {
94
93
  const selection = (0, workflow_resolution_1.selectVariant)(candidates, flags);
95
94
  const selected = files.find(f => f.file === selection.selectedIdentifier) ?? files[0];
96
95
  const filePath = path_1.default.resolve(this.baseDirReal, selected.file);
97
- const stats = (0, fs_1.statSync)(filePath);
96
+ const stats = await promises_1.default.stat(filePath);
98
97
  index.set(id, {
99
98
  id: selected.definition.id,
100
99
  filename: selected.file,
@@ -117,7 +116,7 @@ class FileWorkflowStorage {
117
116
  const filePath = path_1.default.resolve(this.baseDirReal, filename);
118
117
  assertWithinBase(filePath, this.baseDirReal);
119
118
  try {
120
- const stats = (0, fs_1.statSync)(filePath);
119
+ const stats = await promises_1.default.stat(filePath);
121
120
  if (stats.size > this.maxFileSize) {
122
121
  throw new error_handler_1.SecurityError('Workflow file exceeds size limit', 'file-size');
123
122
  }
@@ -10,6 +10,13 @@ const ajv_1 = __importDefault(require("ajv"));
10
10
  const workflow_1 = require("../../types/workflow");
11
11
  const error_handler_1 = require("../../core/error-handler");
12
12
  const workflow_id_policy_1 = require("../../domain/workflow-id-policy");
13
+ function buildModuleValidator() {
14
+ const schemaPath = path_1.default.resolve(__dirname, '../../../spec/workflow.schema.json');
15
+ const schema = JSON.parse(fs_1.default.readFileSync(schemaPath, 'utf-8'));
16
+ const ajv = new ajv_1.default({ allErrors: true, strict: false });
17
+ return ajv.compile(schema);
18
+ }
19
+ const MODULE_WORKFLOW_VALIDATOR = buildModuleValidator();
13
20
  const VALIDATION_ERROR_PREFIX = '[ValidationError]';
14
21
  function reportValidationFailure(workflowId, sourceKind, error) {
15
22
  console.error(`${VALIDATION_ERROR_PREFIX} ${sourceKind}/${workflowId}: ${error}`);
@@ -18,10 +25,7 @@ class SchemaValidatingWorkflowStorage {
18
25
  constructor(inner) {
19
26
  this.inner = inner;
20
27
  this.kind = 'single';
21
- const schemaPath = path_1.default.resolve(__dirname, '../../../spec/workflow.schema.json');
22
- const schema = JSON.parse(fs_1.default.readFileSync(schemaPath, 'utf-8'));
23
- const ajv = new ajv_1.default({ allErrors: true, strict: false });
24
- this.validator = ajv.compile(schema);
28
+ this.validator = MODULE_WORKFLOW_VALIDATOR;
25
29
  }
26
30
  get source() {
27
31
  return this.inner.source;
@@ -81,10 +85,7 @@ class SchemaValidatingCompositeWorkflowStorage {
81
85
  constructor(inner) {
82
86
  this.inner = inner;
83
87
  this.kind = 'composite';
84
- const schemaPath = path_1.default.resolve(__dirname, '../../../spec/workflow.schema.json');
85
- const schema = JSON.parse(fs_1.default.readFileSync(schemaPath, 'utf-8'));
86
- const ajv = new ajv_1.default({ allErrors: true, strict: false });
87
- this.validator = ajv.compile(schema);
88
+ this.validator = MODULE_WORKFLOW_VALIDATOR;
88
89
  }
89
90
  validateDefinition(definition, sourceKind) {
90
91
  const isValid = this.validator(definition);