@renseiai/agentfactory 0.8.7 → 0.8.9

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 (276) hide show
  1. package/dist/src/config/index.d.ts +1 -1
  2. package/dist/src/config/index.d.ts.map +1 -1
  3. package/dist/src/config/index.js +1 -1
  4. package/dist/src/config/repository-config.d.ts +37 -0
  5. package/dist/src/config/repository-config.d.ts.map +1 -1
  6. package/dist/src/config/repository-config.js +47 -0
  7. package/dist/src/config/repository-config.test.js +140 -1
  8. package/dist/src/governor/decision-engine.d.ts +3 -0
  9. package/dist/src/governor/decision-engine.d.ts.map +1 -1
  10. package/dist/src/governor/decision-engine.js +11 -0
  11. package/dist/src/governor/decision-engine.test.js +33 -0
  12. package/dist/src/governor/event-types.d.ts +18 -1
  13. package/dist/src/governor/event-types.d.ts.map +1 -1
  14. package/dist/src/governor/event-types.js +4 -0
  15. package/dist/src/governor/governor-types.d.ts +1 -1
  16. package/dist/src/governor/governor-types.d.ts.map +1 -1
  17. package/dist/src/governor/governor.d.ts +17 -1
  18. package/dist/src/governor/governor.d.ts.map +1 -1
  19. package/dist/src/governor/governor.js +112 -1
  20. package/dist/src/governor/governor.test.js +155 -0
  21. package/dist/src/index.d.ts +1 -0
  22. package/dist/src/index.d.ts.map +1 -1
  23. package/dist/src/index.js +1 -0
  24. package/dist/src/merge-queue/adapters/github-native.d.ts +22 -0
  25. package/dist/src/merge-queue/adapters/github-native.d.ts.map +1 -0
  26. package/dist/src/merge-queue/adapters/github-native.js +243 -0
  27. package/dist/src/merge-queue/adapters/github-native.test.d.ts +2 -0
  28. package/dist/src/merge-queue/adapters/github-native.test.d.ts.map +1 -0
  29. package/dist/src/merge-queue/adapters/github-native.test.js +384 -0
  30. package/dist/src/merge-queue/index.d.ts +18 -0
  31. package/dist/src/merge-queue/index.d.ts.map +1 -0
  32. package/dist/src/merge-queue/index.js +28 -0
  33. package/dist/src/merge-queue/merge-queue.integration.test.d.ts +2 -0
  34. package/dist/src/merge-queue/merge-queue.integration.test.d.ts.map +1 -0
  35. package/dist/src/merge-queue/merge-queue.integration.test.js +128 -0
  36. package/dist/src/merge-queue/types.d.ts +48 -0
  37. package/dist/src/merge-queue/types.d.ts.map +1 -0
  38. package/dist/src/merge-queue/types.js +8 -0
  39. package/dist/src/orchestrator/artifact-tracker.d.ts +93 -0
  40. package/dist/src/orchestrator/artifact-tracker.d.ts.map +1 -0
  41. package/dist/src/orchestrator/artifact-tracker.js +235 -0
  42. package/dist/src/orchestrator/artifact-tracker.test.d.ts +2 -0
  43. package/dist/src/orchestrator/artifact-tracker.test.d.ts.map +1 -0
  44. package/dist/src/orchestrator/artifact-tracker.test.js +189 -0
  45. package/dist/src/orchestrator/context-manager.d.ts +72 -0
  46. package/dist/src/orchestrator/context-manager.d.ts.map +1 -0
  47. package/dist/src/orchestrator/context-manager.js +120 -0
  48. package/dist/src/orchestrator/context-manager.test.d.ts +2 -0
  49. package/dist/src/orchestrator/context-manager.test.d.ts.map +1 -0
  50. package/dist/src/orchestrator/context-manager.test.js +137 -0
  51. package/dist/src/orchestrator/index.d.ts +8 -2
  52. package/dist/src/orchestrator/index.d.ts.map +1 -1
  53. package/dist/src/orchestrator/index.js +8 -1
  54. package/dist/src/orchestrator/issue-tracker-client.d.ts +4 -0
  55. package/dist/src/orchestrator/issue-tracker-client.d.ts.map +1 -1
  56. package/dist/src/orchestrator/orchestrator.d.ts +12 -0
  57. package/dist/src/orchestrator/orchestrator.d.ts.map +1 -1
  58. package/dist/src/orchestrator/orchestrator.js +282 -2
  59. package/dist/src/orchestrator/parse-work-result.d.ts.map +1 -1
  60. package/dist/src/orchestrator/parse-work-result.js +6 -0
  61. package/dist/src/orchestrator/parse-work-result.test.js +19 -0
  62. package/dist/src/orchestrator/state-recovery.d.ts +21 -2
  63. package/dist/src/orchestrator/state-recovery.d.ts.map +1 -1
  64. package/dist/src/orchestrator/state-recovery.js +54 -2
  65. package/dist/src/orchestrator/state-recovery.test.js +106 -2
  66. package/dist/src/orchestrator/state-types.d.ts +62 -0
  67. package/dist/src/orchestrator/state-types.d.ts.map +1 -1
  68. package/dist/src/orchestrator/state-types.js +5 -1
  69. package/dist/src/orchestrator/summary-builder.d.ts +47 -0
  70. package/dist/src/orchestrator/summary-builder.d.ts.map +1 -0
  71. package/dist/src/orchestrator/summary-builder.js +240 -0
  72. package/dist/src/orchestrator/summary-builder.test.d.ts +2 -0
  73. package/dist/src/orchestrator/summary-builder.test.d.ts.map +1 -0
  74. package/dist/src/orchestrator/summary-builder.test.js +236 -0
  75. package/dist/src/orchestrator/types.d.ts +2 -0
  76. package/dist/src/orchestrator/types.d.ts.map +1 -1
  77. package/dist/src/orchestrator/work-types.d.ts +1 -1
  78. package/dist/src/orchestrator/work-types.d.ts.map +1 -1
  79. package/dist/src/providers/index.d.ts +64 -1
  80. package/dist/src/providers/index.d.ts.map +1 -1
  81. package/dist/src/providers/index.js +132 -1
  82. package/dist/src/providers/index.test.js +340 -2
  83. package/dist/src/routing/index.d.ts +7 -0
  84. package/dist/src/routing/index.d.ts.map +1 -0
  85. package/dist/src/routing/index.js +6 -0
  86. package/dist/src/routing/observation-recorder.d.ts +19 -0
  87. package/dist/src/routing/observation-recorder.d.ts.map +1 -0
  88. package/dist/src/routing/observation-recorder.js +73 -0
  89. package/dist/src/routing/observation-recorder.test.d.ts +2 -0
  90. package/dist/src/routing/observation-recorder.test.d.ts.map +1 -0
  91. package/dist/src/routing/observation-recorder.test.js +322 -0
  92. package/dist/src/routing/observation-store.d.ts +40 -0
  93. package/dist/src/routing/observation-store.d.ts.map +1 -0
  94. package/dist/src/routing/observation-store.js +1 -0
  95. package/dist/src/routing/observation-store.test.d.ts +2 -0
  96. package/dist/src/routing/observation-store.test.d.ts.map +1 -0
  97. package/dist/src/routing/observation-store.test.js +138 -0
  98. package/dist/src/routing/posterior-store.d.ts +12 -0
  99. package/dist/src/routing/posterior-store.d.ts.map +1 -0
  100. package/dist/src/routing/posterior-store.js +13 -0
  101. package/dist/src/routing/posterior-store.test.d.ts +2 -0
  102. package/dist/src/routing/posterior-store.test.d.ts.map +1 -0
  103. package/dist/src/routing/posterior-store.test.js +37 -0
  104. package/dist/src/routing/reward.d.ts +16 -0
  105. package/dist/src/routing/reward.d.ts.map +1 -0
  106. package/dist/src/routing/reward.js +29 -0
  107. package/dist/src/routing/reward.test.d.ts +2 -0
  108. package/dist/src/routing/reward.test.d.ts.map +1 -0
  109. package/dist/src/routing/reward.test.js +210 -0
  110. package/dist/src/routing/routing-engine.d.ts +20 -0
  111. package/dist/src/routing/routing-engine.d.ts.map +1 -0
  112. package/dist/src/routing/routing-engine.js +113 -0
  113. package/dist/src/routing/routing-engine.test.d.ts +2 -0
  114. package/dist/src/routing/routing-engine.test.d.ts.map +1 -0
  115. package/dist/src/routing/routing-engine.test.js +310 -0
  116. package/dist/src/routing/types.d.ts +157 -0
  117. package/dist/src/routing/types.d.ts.map +1 -0
  118. package/dist/src/routing/types.js +68 -0
  119. package/dist/src/routing/types.test.d.ts +2 -0
  120. package/dist/src/routing/types.test.d.ts.map +1 -0
  121. package/dist/src/routing/types.test.js +184 -0
  122. package/dist/src/templates/registry.test.js +2 -2
  123. package/dist/src/templates/types.d.ts +5 -0
  124. package/dist/src/templates/types.d.ts.map +1 -1
  125. package/dist/src/templates/types.js +3 -0
  126. package/dist/src/workflow/agent-cancellation.d.ts +37 -0
  127. package/dist/src/workflow/agent-cancellation.d.ts.map +1 -0
  128. package/dist/src/workflow/agent-cancellation.js +41 -0
  129. package/dist/src/workflow/agent-cancellation.test.d.ts +2 -0
  130. package/dist/src/workflow/agent-cancellation.test.d.ts.map +1 -0
  131. package/dist/src/workflow/agent-cancellation.test.js +86 -0
  132. package/dist/src/workflow/branching-router.d.ts +38 -0
  133. package/dist/src/workflow/branching-router.d.ts.map +1 -0
  134. package/dist/src/workflow/branching-router.js +52 -0
  135. package/dist/src/workflow/branching-router.test.d.ts +2 -0
  136. package/dist/src/workflow/branching-router.test.d.ts.map +1 -0
  137. package/dist/src/workflow/branching-router.test.js +209 -0
  138. package/dist/src/workflow/concurrency-semaphore.d.ts +21 -0
  139. package/dist/src/workflow/concurrency-semaphore.d.ts.map +1 -0
  140. package/dist/src/workflow/concurrency-semaphore.js +46 -0
  141. package/dist/src/workflow/concurrency-semaphore.test.d.ts +2 -0
  142. package/dist/src/workflow/concurrency-semaphore.test.d.ts.map +1 -0
  143. package/dist/src/workflow/concurrency-semaphore.test.js +183 -0
  144. package/dist/src/workflow/duration.d.ts +28 -0
  145. package/dist/src/workflow/duration.d.ts.map +1 -0
  146. package/dist/src/workflow/duration.js +57 -0
  147. package/dist/src/workflow/duration.test.d.ts +2 -0
  148. package/dist/src/workflow/duration.test.d.ts.map +1 -0
  149. package/dist/src/workflow/duration.test.js +74 -0
  150. package/dist/src/workflow/expression/ast.d.ts +53 -0
  151. package/dist/src/workflow/expression/ast.d.ts.map +1 -0
  152. package/dist/src/workflow/expression/ast.js +8 -0
  153. package/dist/src/workflow/expression/context.d.ts +40 -0
  154. package/dist/src/workflow/expression/context.d.ts.map +1 -0
  155. package/dist/src/workflow/expression/context.js +37 -0
  156. package/dist/src/workflow/expression/evaluator.d.ts +28 -0
  157. package/dist/src/workflow/expression/evaluator.d.ts.map +1 -0
  158. package/dist/src/workflow/expression/evaluator.js +165 -0
  159. package/dist/src/workflow/expression/evaluator.test.d.ts +2 -0
  160. package/dist/src/workflow/expression/evaluator.test.d.ts.map +1 -0
  161. package/dist/src/workflow/expression/evaluator.test.js +792 -0
  162. package/dist/src/workflow/expression/expression.test.d.ts +2 -0
  163. package/dist/src/workflow/expression/expression.test.d.ts.map +1 -0
  164. package/dist/src/workflow/expression/expression.test.js +516 -0
  165. package/dist/src/workflow/expression/helpers.d.ts +21 -0
  166. package/dist/src/workflow/expression/helpers.d.ts.map +1 -0
  167. package/dist/src/workflow/expression/helpers.js +56 -0
  168. package/dist/src/workflow/expression/index.d.ts +55 -0
  169. package/dist/src/workflow/expression/index.d.ts.map +1 -0
  170. package/dist/src/workflow/expression/index.js +71 -0
  171. package/dist/src/workflow/expression/lexer.d.ts +37 -0
  172. package/dist/src/workflow/expression/lexer.d.ts.map +1 -0
  173. package/dist/src/workflow/expression/lexer.js +166 -0
  174. package/dist/src/workflow/expression/parser.d.ts +23 -0
  175. package/dist/src/workflow/expression/parser.d.ts.map +1 -0
  176. package/dist/src/workflow/expression/parser.js +181 -0
  177. package/dist/src/workflow/gate-state.d.ts +115 -0
  178. package/dist/src/workflow/gate-state.d.ts.map +1 -0
  179. package/dist/src/workflow/gate-state.js +185 -0
  180. package/dist/src/workflow/gate-state.test.d.ts +2 -0
  181. package/dist/src/workflow/gate-state.test.d.ts.map +1 -0
  182. package/dist/src/workflow/gate-state.test.js +251 -0
  183. package/dist/src/workflow/gates/gate-evaluator.d.ts +119 -0
  184. package/dist/src/workflow/gates/gate-evaluator.d.ts.map +1 -0
  185. package/dist/src/workflow/gates/gate-evaluator.js +243 -0
  186. package/dist/src/workflow/gates/gate-evaluator.test.d.ts +2 -0
  187. package/dist/src/workflow/gates/gate-evaluator.test.d.ts.map +1 -0
  188. package/dist/src/workflow/gates/gate-evaluator.test.js +240 -0
  189. package/dist/src/workflow/gates/signal-gate.d.ts +114 -0
  190. package/dist/src/workflow/gates/signal-gate.d.ts.map +1 -0
  191. package/dist/src/workflow/gates/signal-gate.js +216 -0
  192. package/dist/src/workflow/gates/signal-gate.test.d.ts +2 -0
  193. package/dist/src/workflow/gates/signal-gate.test.d.ts.map +1 -0
  194. package/dist/src/workflow/gates/signal-gate.test.js +199 -0
  195. package/dist/src/workflow/gates/timeout-engine.d.ts +96 -0
  196. package/dist/src/workflow/gates/timeout-engine.d.ts.map +1 -0
  197. package/dist/src/workflow/gates/timeout-engine.js +162 -0
  198. package/dist/src/workflow/gates/timeout-engine.test.d.ts +2 -0
  199. package/dist/src/workflow/gates/timeout-engine.test.d.ts.map +1 -0
  200. package/dist/src/workflow/gates/timeout-engine.test.js +186 -0
  201. package/dist/src/workflow/gates/timer-gate.d.ts +125 -0
  202. package/dist/src/workflow/gates/timer-gate.d.ts.map +1 -0
  203. package/dist/src/workflow/gates/timer-gate.js +381 -0
  204. package/dist/src/workflow/gates/timer-gate.test.d.ts +2 -0
  205. package/dist/src/workflow/gates/timer-gate.test.d.ts.map +1 -0
  206. package/dist/src/workflow/gates/timer-gate.test.js +211 -0
  207. package/dist/src/workflow/gates/webhook-gate.d.ts +132 -0
  208. package/dist/src/workflow/gates/webhook-gate.d.ts.map +1 -0
  209. package/dist/src/workflow/gates/webhook-gate.js +216 -0
  210. package/dist/src/workflow/gates/webhook-gate.test.d.ts +2 -0
  211. package/dist/src/workflow/gates/webhook-gate.test.d.ts.map +1 -0
  212. package/dist/src/workflow/gates/webhook-gate.test.js +182 -0
  213. package/dist/src/workflow/index.d.ts +31 -3
  214. package/dist/src/workflow/index.d.ts.map +1 -1
  215. package/dist/src/workflow/index.js +20 -1
  216. package/dist/src/workflow/parallelism-executor.d.ts +25 -0
  217. package/dist/src/workflow/parallelism-executor.d.ts.map +1 -0
  218. package/dist/src/workflow/parallelism-executor.js +53 -0
  219. package/dist/src/workflow/parallelism-executor.test.d.ts +2 -0
  220. package/dist/src/workflow/parallelism-executor.test.d.ts.map +1 -0
  221. package/dist/src/workflow/parallelism-executor.test.js +191 -0
  222. package/dist/src/workflow/parallelism-types.d.ts +80 -0
  223. package/dist/src/workflow/parallelism-types.d.ts.map +1 -0
  224. package/dist/src/workflow/parallelism-types.js +8 -0
  225. package/dist/src/workflow/phase-context-injector.d.ts +29 -0
  226. package/dist/src/workflow/phase-context-injector.d.ts.map +1 -0
  227. package/dist/src/workflow/phase-context-injector.js +43 -0
  228. package/dist/src/workflow/phase-context-injector.test.d.ts +2 -0
  229. package/dist/src/workflow/phase-context-injector.test.d.ts.map +1 -0
  230. package/dist/src/workflow/phase-context-injector.test.js +123 -0
  231. package/dist/src/workflow/phase-output-collector.d.ts +39 -0
  232. package/dist/src/workflow/phase-output-collector.d.ts.map +1 -0
  233. package/dist/src/workflow/phase-output-collector.js +141 -0
  234. package/dist/src/workflow/phase-output-collector.test.d.ts +2 -0
  235. package/dist/src/workflow/phase-output-collector.test.d.ts.map +1 -0
  236. package/dist/src/workflow/phase-output-collector.test.js +179 -0
  237. package/dist/src/workflow/retry-resolver.d.ts +51 -0
  238. package/dist/src/workflow/retry-resolver.d.ts.map +1 -0
  239. package/dist/src/workflow/retry-resolver.js +70 -0
  240. package/dist/src/workflow/retry-resolver.test.d.ts +2 -0
  241. package/dist/src/workflow/retry-resolver.test.d.ts.map +1 -0
  242. package/dist/src/workflow/retry-resolver.test.js +149 -0
  243. package/dist/src/workflow/strategies/fan-in-strategy.d.ts +21 -0
  244. package/dist/src/workflow/strategies/fan-in-strategy.d.ts.map +1 -0
  245. package/dist/src/workflow/strategies/fan-in-strategy.js +92 -0
  246. package/dist/src/workflow/strategies/fan-in-strategy.test.d.ts +2 -0
  247. package/dist/src/workflow/strategies/fan-in-strategy.test.d.ts.map +1 -0
  248. package/dist/src/workflow/strategies/fan-in-strategy.test.js +182 -0
  249. package/dist/src/workflow/strategies/fan-out-strategy.d.ts +16 -0
  250. package/dist/src/workflow/strategies/fan-out-strategy.d.ts.map +1 -0
  251. package/dist/src/workflow/strategies/fan-out-strategy.js +47 -0
  252. package/dist/src/workflow/strategies/fan-out-strategy.test.d.ts +2 -0
  253. package/dist/src/workflow/strategies/fan-out-strategy.test.d.ts.map +1 -0
  254. package/dist/src/workflow/strategies/fan-out-strategy.test.js +97 -0
  255. package/dist/src/workflow/strategies/index.d.ts +4 -0
  256. package/dist/src/workflow/strategies/index.d.ts.map +1 -0
  257. package/dist/src/workflow/strategies/index.js +3 -0
  258. package/dist/src/workflow/strategies/race-strategy.d.ts +19 -0
  259. package/dist/src/workflow/strategies/race-strategy.d.ts.map +1 -0
  260. package/dist/src/workflow/strategies/race-strategy.js +92 -0
  261. package/dist/src/workflow/strategies/race-strategy.test.d.ts +2 -0
  262. package/dist/src/workflow/strategies/race-strategy.test.d.ts.map +1 -0
  263. package/dist/src/workflow/strategies/race-strategy.test.js +318 -0
  264. package/dist/src/workflow/transition-engine.d.ts +3 -1
  265. package/dist/src/workflow/transition-engine.d.ts.map +1 -1
  266. package/dist/src/workflow/transition-engine.js +26 -7
  267. package/dist/src/workflow/transition-engine.test.js +215 -11
  268. package/dist/src/workflow/workflow-registry.d.ts +46 -1
  269. package/dist/src/workflow/workflow-registry.d.ts.map +1 -1
  270. package/dist/src/workflow/workflow-registry.js +74 -0
  271. package/dist/src/workflow/workflow-registry.test.js +54 -0
  272. package/dist/src/workflow/workflow-types.d.ts +330 -12
  273. package/dist/src/workflow/workflow-types.d.ts.map +1 -1
  274. package/dist/src/workflow/workflow-types.js +100 -5
  275. package/dist/src/workflow/workflow-types.test.js +293 -2
  276. package/package.json +2 -2
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Signal Gate Executor
3
+ *
4
+ * Evaluates whether comments or directives satisfy a signal gate.
5
+ * Signal gates pause workflow execution until a matching comment or
6
+ * directive is detected. This module is backward compatible with
7
+ * existing HOLD/RESUME directives from the override-parser system.
8
+ *
9
+ * All evaluator functions are pure (no I/O) — they take inputs and
10
+ * return results without side effects.
11
+ */
12
+ const log = {
13
+ info: (msg, data) => console.log(`[signal-gate] ${msg}`, data ? JSON.stringify(data) : ''),
14
+ warn: (msg, data) => console.warn(`[signal-gate] ${msg}`, data ? JSON.stringify(data) : ''),
15
+ error: (msg, data) => console.error(`[signal-gate] ${msg}`, data ? JSON.stringify(data) : ''),
16
+ debug: (_msg, _data) => { },
17
+ };
18
+ // ============================================
19
+ // Type Guards
20
+ // ============================================
21
+ /**
22
+ * Type guard to validate that a trigger object has the correct shape
23
+ * for a signal gate trigger.
24
+ *
25
+ * A valid SignalGateTrigger must have:
26
+ * - `source`: either 'comment' or 'directive'
27
+ * - `match`: a non-empty string
28
+ *
29
+ * @param trigger - The trigger record to validate
30
+ * @returns True if the trigger is a valid SignalGateTrigger
31
+ */
32
+ export function isSignalGateTrigger(trigger) {
33
+ if (typeof trigger.source !== 'string')
34
+ return false;
35
+ if (trigger.source !== 'comment' && trigger.source !== 'directive')
36
+ return false;
37
+ if (typeof trigger.match !== 'string')
38
+ return false;
39
+ if (trigger.match.length === 0)
40
+ return false;
41
+ return true;
42
+ }
43
+ // ============================================
44
+ // Signal Gate Evaluator
45
+ // ============================================
46
+ /**
47
+ * Extract the first non-empty line from a comment body.
48
+ * Mirrors the directive extraction logic in override-parser.ts.
49
+ *
50
+ * @param body - The full comment body text
51
+ * @returns The trimmed first non-empty line, or empty string if none
52
+ */
53
+ function extractDirectiveLine(body) {
54
+ const lines = body.split('\n');
55
+ for (const line of lines) {
56
+ const trimmed = line.trim();
57
+ if (trimmed.length > 0) {
58
+ return trimmed;
59
+ }
60
+ }
61
+ return '';
62
+ }
63
+ /**
64
+ * Evaluate whether a comment satisfies a signal gate trigger.
65
+ *
66
+ * This is a pure function (no I/O) that checks if a comment matches
67
+ * the signal gate's trigger configuration.
68
+ *
69
+ * Matching rules:
70
+ * - Bot comments are always skipped (returns { matched: false })
71
+ * - When `trigger.source` is 'directive', only the first non-empty line
72
+ * of the comment is checked (consistent with override-parser.ts)
73
+ * - When `trigger.source` is 'comment', the full trimmed comment text is checked
74
+ * - The `trigger.match` string is first tried as an exact case-insensitive match,
75
+ * then as a regex pattern (case-insensitive)
76
+ *
77
+ * @param gate - The gate definition containing a signal trigger
78
+ * @param comment - The full comment body text
79
+ * @param isBot - Whether the comment was authored by a bot
80
+ * @returns A SignalGateResult indicating whether the comment matched
81
+ */
82
+ export function evaluateSignalGate(gate, comment, isBot) {
83
+ // Bot comments are always ignored
84
+ if (isBot) {
85
+ log.debug('Skipping bot comment for signal gate', { gateName: gate.name });
86
+ return { matched: false };
87
+ }
88
+ // Validate trigger shape
89
+ if (!isSignalGateTrigger(gate.trigger)) {
90
+ log.warn('Gate has invalid signal trigger configuration', { gateName: gate.name, trigger: gate.trigger });
91
+ return { matched: false };
92
+ }
93
+ const trigger = gate.trigger;
94
+ // Determine the text to match against based on trigger source
95
+ let textToMatch;
96
+ if (trigger.source === 'directive') {
97
+ textToMatch = extractDirectiveLine(comment);
98
+ }
99
+ else {
100
+ textToMatch = comment.trim();
101
+ }
102
+ // Empty text cannot match
103
+ if (textToMatch.length === 0) {
104
+ return { matched: false };
105
+ }
106
+ // Try exact case-insensitive match first
107
+ if (textToMatch.toLowerCase() === trigger.match.toLowerCase()) {
108
+ log.debug('Signal gate matched (exact)', { gateName: gate.name, source: textToMatch });
109
+ return { matched: true, source: textToMatch };
110
+ }
111
+ // Try regex match (case-insensitive)
112
+ try {
113
+ const regex = new RegExp(trigger.match, 'i');
114
+ const match = textToMatch.match(regex);
115
+ if (match) {
116
+ log.debug('Signal gate matched (regex)', { gateName: gate.name, source: textToMatch });
117
+ return { matched: true, source: textToMatch };
118
+ }
119
+ }
120
+ catch {
121
+ // Invalid regex pattern — treat as exact match only (already tried above)
122
+ log.warn('Invalid regex in signal gate trigger match', { gateName: gate.name, match: trigger.match });
123
+ }
124
+ return { matched: false };
125
+ }
126
+ // ============================================
127
+ // Workflow Query Helpers
128
+ // ============================================
129
+ /**
130
+ * Get all signal gates from a workflow definition that apply to a given phase.
131
+ *
132
+ * Filters gates by:
133
+ * 1. `type === 'signal'`
134
+ * 2. `appliesTo` includes the given phase name, OR `appliesTo` is not defined
135
+ * (gate applies to all phases)
136
+ *
137
+ * @param workflow - The workflow definition containing gate configurations
138
+ * @param phase - The phase name to filter by
139
+ * @returns Array of GateDefinition objects that are signal gates applicable to the phase
140
+ */
141
+ export function getApplicableSignalGates(workflow, phase) {
142
+ if (!workflow.gates || workflow.gates.length === 0) {
143
+ return [];
144
+ }
145
+ return workflow.gates.filter((gate) => {
146
+ // Must be a signal gate
147
+ if (gate.type !== 'signal')
148
+ return false;
149
+ // If appliesTo is defined, the phase must be in the list
150
+ if (gate.appliesTo && gate.appliesTo.length > 0) {
151
+ return gate.appliesTo.includes(phase);
152
+ }
153
+ // No appliesTo restriction — applies to all phases
154
+ return true;
155
+ });
156
+ }
157
+ // ============================================
158
+ // HOLD/RESUME Backward Compatibility
159
+ // ============================================
160
+ /**
161
+ * Name used for the implicit HOLD gate created for backward compatibility
162
+ */
163
+ export const IMPLICIT_HOLD_GATE_NAME = '__implicit-hold';
164
+ /**
165
+ * Name used for the implicit RESUME gate created for backward compatibility
166
+ */
167
+ export const IMPLICIT_RESUME_GATE_NAME = '__implicit-resume';
168
+ /**
169
+ * Create an implicit signal gate definition that matches the HOLD directive.
170
+ *
171
+ * This provides backward compatibility with the existing HOLD/RESUME system.
172
+ * When a HOLD directive is detected and no explicit signal gate is defined,
173
+ * this creates a gate that will pause the workflow until a RESUME directive
174
+ * is received.
175
+ *
176
+ * The created gate matches:
177
+ * - Directive source: first line of comment
178
+ * - Pattern: `^hold(?:\s*[---]\s*(.+))?$` (matches HOLD or HOLD -- reason)
179
+ *
180
+ * @returns A GateDefinition representing the implicit HOLD gate
181
+ */
182
+ export function createImplicitHoldGate() {
183
+ return {
184
+ name: IMPLICIT_HOLD_GATE_NAME,
185
+ description: 'Implicit gate created for backward-compatible HOLD directive',
186
+ type: 'signal',
187
+ trigger: {
188
+ source: 'directive',
189
+ match: '^hold(?:\\s*[\\u2014\\u2013-]\\s*(.+))?$',
190
+ },
191
+ };
192
+ }
193
+ /**
194
+ * Create an implicit signal gate definition that matches the RESUME directive.
195
+ *
196
+ * This is the counterpart to the implicit HOLD gate. When a workflow is
197
+ * paused by a HOLD directive, this gate defines the condition that will
198
+ * release the hold — receiving a RESUME directive.
199
+ *
200
+ * The created gate matches:
201
+ * - Directive source: first line of comment
202
+ * - Pattern: `^resume$` (exact match for RESUME directive)
203
+ *
204
+ * @returns A GateDefinition representing the implicit RESUME gate
205
+ */
206
+ export function createImplicitResumeGate() {
207
+ return {
208
+ name: IMPLICIT_RESUME_GATE_NAME,
209
+ description: 'Implicit gate created for backward-compatible RESUME directive',
210
+ type: 'signal',
211
+ trigger: {
212
+ source: 'directive',
213
+ match: '^resume$',
214
+ },
215
+ };
216
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=signal-gate.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signal-gate.test.d.ts","sourceRoot":"","sources":["../../../../src/workflow/gates/signal-gate.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,199 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { evaluateSignalGate, isSignalGateTrigger, getApplicableSignalGates, createImplicitHoldGate, createImplicitResumeGate, IMPLICIT_HOLD_GATE_NAME, IMPLICIT_RESUME_GATE_NAME, } from './signal-gate.js';
3
+ // ---------------------------------------------------------------------------
4
+ // Helpers
5
+ // ---------------------------------------------------------------------------
6
+ function makeGateDefinition(overrides = {}) {
7
+ return {
8
+ name: 'test-gate',
9
+ type: 'signal',
10
+ trigger: { source: 'comment', match: 'APPROVE' },
11
+ ...overrides,
12
+ };
13
+ }
14
+ function makeWorkflow(gates = []) {
15
+ return {
16
+ apiVersion: 'v1.1',
17
+ kind: 'WorkflowDefinition',
18
+ metadata: { name: 'test-workflow' },
19
+ phases: [{ name: 'development', template: 'dev' }],
20
+ transitions: [{ from: 'Backlog', to: 'development' }],
21
+ gates,
22
+ };
23
+ }
24
+ // ---------------------------------------------------------------------------
25
+ // evaluateSignalGate
26
+ // ---------------------------------------------------------------------------
27
+ describe('evaluateSignalGate', () => {
28
+ it('matches exact comment text (case-insensitive)', () => {
29
+ const gate = makeGateDefinition({ trigger: { source: 'comment', match: 'APPROVE' } });
30
+ const result = evaluateSignalGate(gate, 'approve', false);
31
+ expect(result.matched).toBe(true);
32
+ expect(result.source).toBe('approve');
33
+ });
34
+ it('matches exact comment text (exact case)', () => {
35
+ const gate = makeGateDefinition({ trigger: { source: 'comment', match: 'APPROVE' } });
36
+ const result = evaluateSignalGate(gate, 'APPROVE', false);
37
+ expect(result.matched).toBe(true);
38
+ expect(result.source).toBe('APPROVE');
39
+ });
40
+ it('matches regex pattern', () => {
41
+ const gate = makeGateDefinition({ trigger: { source: 'comment', match: '^LGTM.*$' } });
42
+ const result = evaluateSignalGate(gate, 'LGTM looks good', false);
43
+ expect(result.matched).toBe(true);
44
+ expect(result.source).toBe('LGTM looks good');
45
+ });
46
+ it('handles directive-only mode (first non-empty line)', () => {
47
+ const gate = makeGateDefinition({ trigger: { source: 'directive', match: 'APPROVE' } });
48
+ const result = evaluateSignalGate(gate, 'APPROVE\nsome other text', false);
49
+ expect(result.matched).toBe(true);
50
+ expect(result.source).toBe('APPROVE');
51
+ });
52
+ it('skips bot comments', () => {
53
+ const gate = makeGateDefinition({ trigger: { source: 'comment', match: 'APPROVE' } });
54
+ const result = evaluateSignalGate(gate, 'APPROVE', true);
55
+ expect(result.matched).toBe(false);
56
+ });
57
+ it('handles invalid regex gracefully (falls back to exact match only)', () => {
58
+ const gate = makeGateDefinition({ trigger: { source: 'comment', match: '[invalid(' } });
59
+ const result = evaluateSignalGate(gate, 'something else', false);
60
+ expect(result.matched).toBe(false);
61
+ });
62
+ it('is case-insensitive for regex matching', () => {
63
+ const gate = makeGateDefinition({ trigger: { source: 'comment', match: '^approve$' } });
64
+ const result = evaluateSignalGate(gate, 'APPROVE', false);
65
+ expect(result.matched).toBe(true);
66
+ });
67
+ it('returns not matched for empty comment with directive source', () => {
68
+ const gate = makeGateDefinition({ trigger: { source: 'directive', match: 'APPROVE' } });
69
+ const result = evaluateSignalGate(gate, '', false);
70
+ expect(result.matched).toBe(false);
71
+ });
72
+ it('returns not matched for non-matching comment', () => {
73
+ const gate = makeGateDefinition({ trigger: { source: 'comment', match: 'APPROVE' } });
74
+ const result = evaluateSignalGate(gate, 'I reject this', false);
75
+ expect(result.matched).toBe(false);
76
+ });
77
+ it('returns not matched for invalid trigger configuration', () => {
78
+ const gate = makeGateDefinition({ trigger: { invalid: true } });
79
+ const result = evaluateSignalGate(gate, 'APPROVE', false);
80
+ expect(result.matched).toBe(false);
81
+ });
82
+ it('trims whitespace from comment when source is comment', () => {
83
+ const gate = makeGateDefinition({ trigger: { source: 'comment', match: 'APPROVE' } });
84
+ const result = evaluateSignalGate(gate, ' APPROVE ', false);
85
+ expect(result.matched).toBe(true);
86
+ });
87
+ });
88
+ // ---------------------------------------------------------------------------
89
+ // isSignalGateTrigger
90
+ // ---------------------------------------------------------------------------
91
+ describe('isSignalGateTrigger', () => {
92
+ it('returns true for valid signal trigger', () => {
93
+ expect(isSignalGateTrigger({ source: 'comment', match: 'APPROVE' })).toBe(true);
94
+ });
95
+ it('returns true for directive source', () => {
96
+ expect(isSignalGateTrigger({ source: 'directive', match: 'HOLD' })).toBe(true);
97
+ });
98
+ it('returns false for missing source', () => {
99
+ expect(isSignalGateTrigger({ match: 'APPROVE' })).toBe(false);
100
+ });
101
+ it('returns false for invalid source type', () => {
102
+ expect(isSignalGateTrigger({ source: 'webhook', match: 'APPROVE' })).toBe(false);
103
+ });
104
+ it('returns false for missing match', () => {
105
+ expect(isSignalGateTrigger({ source: 'comment' })).toBe(false);
106
+ });
107
+ it('returns false for empty match', () => {
108
+ expect(isSignalGateTrigger({ source: 'comment', match: '' })).toBe(false);
109
+ });
110
+ });
111
+ // ---------------------------------------------------------------------------
112
+ // getApplicableSignalGates
113
+ // ---------------------------------------------------------------------------
114
+ describe('getApplicableSignalGates', () => {
115
+ it('filters by type=signal and appliesTo', () => {
116
+ const gates = [
117
+ makeGateDefinition({ name: 'signal-1', type: 'signal', appliesTo: ['development'] }),
118
+ makeGateDefinition({ name: 'timer-1', type: 'timer', appliesTo: ['development'] }),
119
+ makeGateDefinition({ name: 'signal-2', type: 'signal', appliesTo: ['qa'] }),
120
+ ];
121
+ const workflow = makeWorkflow(gates);
122
+ const result = getApplicableSignalGates(workflow, 'development');
123
+ expect(result).toHaveLength(1);
124
+ expect(result[0].name).toBe('signal-1');
125
+ });
126
+ it('handles no gates', () => {
127
+ const workflow = makeWorkflow([]);
128
+ const result = getApplicableSignalGates(workflow, 'development');
129
+ expect(result).toEqual([]);
130
+ });
131
+ it('handles undefined gates', () => {
132
+ const workflow = makeWorkflow();
133
+ delete workflow.gates;
134
+ const result = getApplicableSignalGates(workflow, 'development');
135
+ expect(result).toEqual([]);
136
+ });
137
+ it('returns gates with no appliesTo (applies to all phases)', () => {
138
+ const gates = [
139
+ makeGateDefinition({ name: 'global-gate', type: 'signal' }),
140
+ ];
141
+ const workflow = makeWorkflow(gates);
142
+ const result = getApplicableSignalGates(workflow, 'any-phase');
143
+ expect(result).toHaveLength(1);
144
+ expect(result[0].name).toBe('global-gate');
145
+ });
146
+ it('returns gates with empty appliesTo array (applies to all phases)', () => {
147
+ const gates = [
148
+ makeGateDefinition({ name: 'global-gate', type: 'signal', appliesTo: [] }),
149
+ ];
150
+ const workflow = makeWorkflow(gates);
151
+ const result = getApplicableSignalGates(workflow, 'any-phase');
152
+ expect(result).toHaveLength(1);
153
+ });
154
+ });
155
+ // ---------------------------------------------------------------------------
156
+ // createImplicitHoldGate
157
+ // ---------------------------------------------------------------------------
158
+ describe('createImplicitHoldGate', () => {
159
+ it('creates a gate matching HOLD directive', () => {
160
+ const gate = createImplicitHoldGate();
161
+ expect(gate.name).toBe(IMPLICIT_HOLD_GATE_NAME);
162
+ expect(gate.type).toBe('signal');
163
+ // Should match "HOLD"
164
+ const result = evaluateSignalGate(gate, 'HOLD', false);
165
+ expect(result.matched).toBe(true);
166
+ });
167
+ it('matches HOLD with reason (HOLD -- reason)', () => {
168
+ const gate = createImplicitHoldGate();
169
+ const result = evaluateSignalGate(gate, 'HOLD -- waiting for design review', false);
170
+ expect(result.matched).toBe(true);
171
+ });
172
+ it('matches HOLD with em-dash', () => {
173
+ const gate = createImplicitHoldGate();
174
+ const result = evaluateSignalGate(gate, 'HOLD \u2014 some reason', false);
175
+ expect(result.matched).toBe(true);
176
+ });
177
+ });
178
+ // ---------------------------------------------------------------------------
179
+ // createImplicitResumeGate
180
+ // ---------------------------------------------------------------------------
181
+ describe('createImplicitResumeGate', () => {
182
+ it('creates a gate matching RESUME directive', () => {
183
+ const gate = createImplicitResumeGate();
184
+ expect(gate.name).toBe(IMPLICIT_RESUME_GATE_NAME);
185
+ expect(gate.type).toBe('signal');
186
+ const result = evaluateSignalGate(gate, 'RESUME', false);
187
+ expect(result.matched).toBe(true);
188
+ });
189
+ it('matches case-insensitively', () => {
190
+ const gate = createImplicitResumeGate();
191
+ const result = evaluateSignalGate(gate, 'resume', false);
192
+ expect(result.matched).toBe(true);
193
+ });
194
+ it('does not match RESUME with extra text', () => {
195
+ const gate = createImplicitResumeGate();
196
+ const result = evaluateSignalGate(gate, 'RESUME now please', false);
197
+ expect(result.matched).toBe(false);
198
+ });
199
+ });
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Gate Timeout Engine
3
+ *
4
+ * Cross-cutting timeout engine that monitors active gates and determines
5
+ * what action to take when deadlines expire. Every gate type (signal, timer,
6
+ * webhook) can have an optional timeout with an action of 'escalate', 'skip',
7
+ * or 'fail'. This engine is the shared component that checks deadlines and
8
+ * returns the action to take.
9
+ *
10
+ * The main entry point is `processGateTimeouts()`, which is called by the
11
+ * governor on each poll cycle. The lower-level functions (`checkGateTimeout`,
12
+ * `checkAllGateTimeouts`, `resolveTimeoutAction`) are pure and exported for
13
+ * direct use and testability.
14
+ */
15
+ import type { GateState, GateStorage } from '../gate-state.js';
16
+ /**
17
+ * Result of checking a single gate for timeout.
18
+ * If `timedOut` is false, no action field is present.
19
+ * If `timedOut` is true, `action` indicates what should happen.
20
+ */
21
+ export interface TimeoutCheckResult {
22
+ timedOut: boolean;
23
+ action?: 'escalate' | 'skip' | 'fail';
24
+ }
25
+ /**
26
+ * A gate that has been determined to have timed out, paired with
27
+ * the action that should be taken.
28
+ */
29
+ export interface TimedOutGate {
30
+ gateState: GateState;
31
+ action: 'escalate' | 'skip' | 'fail';
32
+ }
33
+ /**
34
+ * The resolution produced for a timed-out gate, containing all the
35
+ * information the governor needs to act on the timeout.
36
+ */
37
+ export interface TimeoutResolution {
38
+ type: 'escalate' | 'skip' | 'fail';
39
+ issueId: string;
40
+ gateName: string;
41
+ reason: string;
42
+ }
43
+ /**
44
+ * Check whether a single gate has timed out.
45
+ *
46
+ * Pure function that compares the current time against the gate's
47
+ * `timeoutDeadline`. Returns `{ timedOut: false }` if no deadline
48
+ * exists or the deadline has not yet passed.
49
+ *
50
+ * @param gateState - The gate state to check
51
+ * @param now - Current time in epoch ms (defaults to Date.now() for testability)
52
+ * @returns The timeout check result
53
+ */
54
+ export declare function checkGateTimeout(gateState: GateState, now?: number): TimeoutCheckResult;
55
+ /**
56
+ * Check multiple gates for timeouts.
57
+ *
58
+ * Pure function that filters a list of gate states down to only those
59
+ * that have timed out, returning each with its configured timeout action.
60
+ *
61
+ * @param gates - Array of gate states to check
62
+ * @param now - Current time in epoch ms (defaults to Date.now() for testability)
63
+ * @returns Array of gates that have timed out, with their actions
64
+ */
65
+ export declare function checkAllGateTimeouts(gates: GateState[], now?: number): TimedOutGate[];
66
+ /**
67
+ * Determine the resolution for a timeout action.
68
+ *
69
+ * Pure function that maps a timeout action to a structured resolution
70
+ * containing the action type, issue context, and a human-readable reason.
71
+ *
72
+ * - `escalate` - The governor should advance the escalation strategy
73
+ * - `skip` - The governor should skip the gate and continue the workflow
74
+ * - `fail` - The governor should fail the workflow
75
+ *
76
+ * @param action - The timeout action to resolve
77
+ * @param issueId - The issue identifier associated with the gate
78
+ * @param gateName - The name of the gate that timed out
79
+ * @returns The structured timeout resolution
80
+ */
81
+ export declare function resolveTimeoutAction(action: 'escalate' | 'skip' | 'fail', issueId: string, gateName: string): TimeoutResolution;
82
+ /**
83
+ * Process gate timeouts for a set of active gates.
84
+ *
85
+ * This is the main entry point called by the governor on each poll cycle.
86
+ * It checks all provided gates for timeouts, marks timed-out gates via
87
+ * the storage adapter (using `timeoutGate()` from gate-state.ts), and
88
+ * returns an array of timeout resolutions for the caller to act on.
89
+ *
90
+ * @param activeGates - Array of currently active gate states to check
91
+ * @param storage - The gate storage adapter for persisting state changes
92
+ * @param now - Current time in epoch ms (defaults to Date.now() for testability)
93
+ * @returns Array of timeout resolutions for the governor to process
94
+ */
95
+ export declare function processGateTimeouts(activeGates: GateState[], storage: GateStorage, now?: number): Promise<TimeoutResolution[]>;
96
+ //# sourceMappingURL=timeout-engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timeout-engine.d.ts","sourceRoot":"","sources":["../../../../src/workflow/gates/timeout-engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAqB9D;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,OAAO,CAAA;IACjB,MAAM,CAAC,EAAE,UAAU,GAAG,MAAM,GAAG,MAAM,CAAA;CACtC;AAED;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,SAAS,CAAA;IACpB,MAAM,EAAE,UAAU,GAAG,MAAM,GAAG,MAAM,CAAA;CACrC;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,UAAU,GAAG,MAAM,GAAG,MAAM,CAAA;IAClC,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;CACf;AAMD;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,SAAS,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,kBAAkB,CAkBvF;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,YAAY,EAAE,CAYrF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,UAAU,GAAG,MAAM,GAAG,MAAM,EACpC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GACf,iBAAiB,CAwBnB;AAMD;;;;;;;;;;;;GAYG;AACH,wBAAsB,mBAAmB,CACvC,WAAW,EAAE,SAAS,EAAE,EACxB,OAAO,EAAE,WAAW,EACpB,GAAG,CAAC,EAAE,MAAM,GACX,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAsC9B"}
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Gate Timeout Engine
3
+ *
4
+ * Cross-cutting timeout engine that monitors active gates and determines
5
+ * what action to take when deadlines expire. Every gate type (signal, timer,
6
+ * webhook) can have an optional timeout with an action of 'escalate', 'skip',
7
+ * or 'fail'. This engine is the shared component that checks deadlines and
8
+ * returns the action to take.
9
+ *
10
+ * The main entry point is `processGateTimeouts()`, which is called by the
11
+ * governor on each poll cycle. The lower-level functions (`checkGateTimeout`,
12
+ * `checkAllGateTimeouts`, `resolveTimeoutAction`) are pure and exported for
13
+ * direct use and testability.
14
+ */
15
+ import { timeoutGate } from '../gate-state.js';
16
+ // ============================================
17
+ // Logger
18
+ // ============================================
19
+ const log = {
20
+ info: (msg, data) => console.log(`[timeout-engine] ${msg}`, data ? JSON.stringify(data) : ''),
21
+ warn: (msg, data) => console.warn(`[timeout-engine] ${msg}`, data ? JSON.stringify(data) : ''),
22
+ error: (msg, data) => console.error(`[timeout-engine] ${msg}`, data ? JSON.stringify(data) : ''),
23
+ debug: (_msg, _data) => { },
24
+ };
25
+ // ============================================
26
+ // Pure Functions
27
+ // ============================================
28
+ /**
29
+ * Check whether a single gate has timed out.
30
+ *
31
+ * Pure function that compares the current time against the gate's
32
+ * `timeoutDeadline`. Returns `{ timedOut: false }` if no deadline
33
+ * exists or the deadline has not yet passed.
34
+ *
35
+ * @param gateState - The gate state to check
36
+ * @param now - Current time in epoch ms (defaults to Date.now() for testability)
37
+ * @returns The timeout check result
38
+ */
39
+ export function checkGateTimeout(gateState, now) {
40
+ const currentTime = now ?? Date.now();
41
+ // No deadline configured — cannot time out
42
+ if (gateState.timeoutDeadline == null) {
43
+ return { timedOut: false };
44
+ }
45
+ // Deadline has not passed yet
46
+ if (currentTime < gateState.timeoutDeadline) {
47
+ return { timedOut: false };
48
+ }
49
+ // Deadline has expired — return the configured action (default to 'fail')
50
+ return {
51
+ timedOut: true,
52
+ action: gateState.timeoutAction ?? 'fail',
53
+ };
54
+ }
55
+ /**
56
+ * Check multiple gates for timeouts.
57
+ *
58
+ * Pure function that filters a list of gate states down to only those
59
+ * that have timed out, returning each with its configured timeout action.
60
+ *
61
+ * @param gates - Array of gate states to check
62
+ * @param now - Current time in epoch ms (defaults to Date.now() for testability)
63
+ * @returns Array of gates that have timed out, with their actions
64
+ */
65
+ export function checkAllGateTimeouts(gates, now) {
66
+ const currentTime = now ?? Date.now();
67
+ const timedOut = [];
68
+ for (const gateState of gates) {
69
+ const result = checkGateTimeout(gateState, currentTime);
70
+ if (result.timedOut && result.action) {
71
+ timedOut.push({ gateState, action: result.action });
72
+ }
73
+ }
74
+ return timedOut;
75
+ }
76
+ /**
77
+ * Determine the resolution for a timeout action.
78
+ *
79
+ * Pure function that maps a timeout action to a structured resolution
80
+ * containing the action type, issue context, and a human-readable reason.
81
+ *
82
+ * - `escalate` - The governor should advance the escalation strategy
83
+ * - `skip` - The governor should skip the gate and continue the workflow
84
+ * - `fail` - The governor should fail the workflow
85
+ *
86
+ * @param action - The timeout action to resolve
87
+ * @param issueId - The issue identifier associated with the gate
88
+ * @param gateName - The name of the gate that timed out
89
+ * @returns The structured timeout resolution
90
+ */
91
+ export function resolveTimeoutAction(action, issueId, gateName) {
92
+ switch (action) {
93
+ case 'escalate':
94
+ return {
95
+ type: 'escalate',
96
+ issueId,
97
+ gateName,
98
+ reason: `Gate ${gateName} timed out — escalating`,
99
+ };
100
+ case 'skip':
101
+ return {
102
+ type: 'skip',
103
+ issueId,
104
+ gateName,
105
+ reason: `Gate ${gateName} timed out — skipping gate`,
106
+ };
107
+ case 'fail':
108
+ return {
109
+ type: 'fail',
110
+ issueId,
111
+ gateName,
112
+ reason: `Gate ${gateName} timed out — failing workflow`,
113
+ };
114
+ }
115
+ }
116
+ // ============================================
117
+ // I/O Function
118
+ // ============================================
119
+ /**
120
+ * Process gate timeouts for a set of active gates.
121
+ *
122
+ * This is the main entry point called by the governor on each poll cycle.
123
+ * It checks all provided gates for timeouts, marks timed-out gates via
124
+ * the storage adapter (using `timeoutGate()` from gate-state.ts), and
125
+ * returns an array of timeout resolutions for the caller to act on.
126
+ *
127
+ * @param activeGates - Array of currently active gate states to check
128
+ * @param storage - The gate storage adapter for persisting state changes
129
+ * @param now - Current time in epoch ms (defaults to Date.now() for testability)
130
+ * @returns Array of timeout resolutions for the governor to process
131
+ */
132
+ export async function processGateTimeouts(activeGates, storage, now) {
133
+ const timedOutGates = checkAllGateTimeouts(activeGates, now);
134
+ if (timedOutGates.length === 0) {
135
+ return [];
136
+ }
137
+ log.info('Processing gate timeouts', {
138
+ count: timedOutGates.length,
139
+ gates: timedOutGates.map(g => g.gateState.gateName),
140
+ });
141
+ const resolutions = [];
142
+ for (const { gateState, action } of timedOutGates) {
143
+ // Mark the gate as timed-out in storage
144
+ const updated = await timeoutGate(gateState.issueId, gateState.gateName, storage);
145
+ if (!updated) {
146
+ log.warn('Failed to mark gate as timed-out (gate not found or not active)', {
147
+ issueId: gateState.issueId,
148
+ gateName: gateState.gateName,
149
+ });
150
+ continue;
151
+ }
152
+ const resolution = resolveTimeoutAction(action, gateState.issueId, gateState.gateName);
153
+ resolutions.push(resolution);
154
+ log.info('Gate timeout resolved', {
155
+ issueId: gateState.issueId,
156
+ gateName: gateState.gateName,
157
+ action,
158
+ reason: resolution.reason,
159
+ });
160
+ }
161
+ return resolutions;
162
+ }