@renseiai/agentfactory 0.8.8 → 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 (178) 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 +23 -0
  5. package/dist/src/config/repository-config.d.ts.map +1 -1
  6. package/dist/src/config/repository-config.js +27 -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/governor-types.d.ts +1 -1
  13. package/dist/src/governor/governor-types.d.ts.map +1 -1
  14. package/dist/src/governor/governor.d.ts +17 -1
  15. package/dist/src/governor/governor.d.ts.map +1 -1
  16. package/dist/src/governor/governor.js +112 -1
  17. package/dist/src/governor/governor.test.js +155 -0
  18. package/dist/src/index.d.ts +1 -0
  19. package/dist/src/index.d.ts.map +1 -1
  20. package/dist/src/index.js +1 -0
  21. package/dist/src/orchestrator/issue-tracker-client.d.ts +4 -0
  22. package/dist/src/orchestrator/issue-tracker-client.d.ts.map +1 -1
  23. package/dist/src/orchestrator/orchestrator.d.ts.map +1 -1
  24. package/dist/src/orchestrator/orchestrator.js +24 -0
  25. package/dist/src/orchestrator/parse-work-result.d.ts.map +1 -1
  26. package/dist/src/orchestrator/parse-work-result.js +6 -0
  27. package/dist/src/orchestrator/parse-work-result.test.js +19 -0
  28. package/dist/src/providers/index.d.ts +64 -1
  29. package/dist/src/providers/index.d.ts.map +1 -1
  30. package/dist/src/providers/index.js +132 -1
  31. package/dist/src/providers/index.test.js +340 -2
  32. package/dist/src/routing/index.d.ts +7 -0
  33. package/dist/src/routing/index.d.ts.map +1 -0
  34. package/dist/src/routing/index.js +6 -0
  35. package/dist/src/routing/observation-recorder.d.ts +19 -0
  36. package/dist/src/routing/observation-recorder.d.ts.map +1 -0
  37. package/dist/src/routing/observation-recorder.js +73 -0
  38. package/dist/src/routing/observation-recorder.test.d.ts +2 -0
  39. package/dist/src/routing/observation-recorder.test.d.ts.map +1 -0
  40. package/dist/src/routing/observation-recorder.test.js +322 -0
  41. package/dist/src/routing/observation-store.d.ts +40 -0
  42. package/dist/src/routing/observation-store.d.ts.map +1 -0
  43. package/dist/src/routing/observation-store.js +1 -0
  44. package/dist/src/routing/observation-store.test.d.ts +2 -0
  45. package/dist/src/routing/observation-store.test.d.ts.map +1 -0
  46. package/dist/src/routing/observation-store.test.js +138 -0
  47. package/dist/src/routing/posterior-store.d.ts +12 -0
  48. package/dist/src/routing/posterior-store.d.ts.map +1 -0
  49. package/dist/src/routing/posterior-store.js +13 -0
  50. package/dist/src/routing/posterior-store.test.d.ts +2 -0
  51. package/dist/src/routing/posterior-store.test.d.ts.map +1 -0
  52. package/dist/src/routing/posterior-store.test.js +37 -0
  53. package/dist/src/routing/reward.d.ts +16 -0
  54. package/dist/src/routing/reward.d.ts.map +1 -0
  55. package/dist/src/routing/reward.js +29 -0
  56. package/dist/src/routing/reward.test.d.ts +2 -0
  57. package/dist/src/routing/reward.test.d.ts.map +1 -0
  58. package/dist/src/routing/reward.test.js +210 -0
  59. package/dist/src/routing/routing-engine.d.ts +20 -0
  60. package/dist/src/routing/routing-engine.d.ts.map +1 -0
  61. package/dist/src/routing/routing-engine.js +113 -0
  62. package/dist/src/routing/routing-engine.test.d.ts +2 -0
  63. package/dist/src/routing/routing-engine.test.d.ts.map +1 -0
  64. package/dist/src/routing/routing-engine.test.js +310 -0
  65. package/dist/src/routing/types.d.ts +157 -0
  66. package/dist/src/routing/types.d.ts.map +1 -0
  67. package/dist/src/routing/types.js +68 -0
  68. package/dist/src/routing/types.test.d.ts +2 -0
  69. package/dist/src/routing/types.test.d.ts.map +1 -0
  70. package/dist/src/routing/types.test.js +184 -0
  71. package/dist/src/templates/types.d.ts +3 -0
  72. package/dist/src/templates/types.d.ts.map +1 -1
  73. package/dist/src/templates/types.js +2 -0
  74. package/dist/src/workflow/agent-cancellation.d.ts +37 -0
  75. package/dist/src/workflow/agent-cancellation.d.ts.map +1 -0
  76. package/dist/src/workflow/agent-cancellation.js +41 -0
  77. package/dist/src/workflow/agent-cancellation.test.d.ts +2 -0
  78. package/dist/src/workflow/agent-cancellation.test.d.ts.map +1 -0
  79. package/dist/src/workflow/agent-cancellation.test.js +86 -0
  80. package/dist/src/workflow/concurrency-semaphore.d.ts +21 -0
  81. package/dist/src/workflow/concurrency-semaphore.d.ts.map +1 -0
  82. package/dist/src/workflow/concurrency-semaphore.js +46 -0
  83. package/dist/src/workflow/concurrency-semaphore.test.d.ts +2 -0
  84. package/dist/src/workflow/concurrency-semaphore.test.d.ts.map +1 -0
  85. package/dist/src/workflow/concurrency-semaphore.test.js +183 -0
  86. package/dist/src/workflow/gate-state.d.ts +115 -0
  87. package/dist/src/workflow/gate-state.d.ts.map +1 -0
  88. package/dist/src/workflow/gate-state.js +185 -0
  89. package/dist/src/workflow/gate-state.test.d.ts +2 -0
  90. package/dist/src/workflow/gate-state.test.d.ts.map +1 -0
  91. package/dist/src/workflow/gate-state.test.js +251 -0
  92. package/dist/src/workflow/gates/gate-evaluator.d.ts +119 -0
  93. package/dist/src/workflow/gates/gate-evaluator.d.ts.map +1 -0
  94. package/dist/src/workflow/gates/gate-evaluator.js +243 -0
  95. package/dist/src/workflow/gates/gate-evaluator.test.d.ts +2 -0
  96. package/dist/src/workflow/gates/gate-evaluator.test.d.ts.map +1 -0
  97. package/dist/src/workflow/gates/gate-evaluator.test.js +240 -0
  98. package/dist/src/workflow/gates/signal-gate.d.ts +114 -0
  99. package/dist/src/workflow/gates/signal-gate.d.ts.map +1 -0
  100. package/dist/src/workflow/gates/signal-gate.js +216 -0
  101. package/dist/src/workflow/gates/signal-gate.test.d.ts +2 -0
  102. package/dist/src/workflow/gates/signal-gate.test.d.ts.map +1 -0
  103. package/dist/src/workflow/gates/signal-gate.test.js +199 -0
  104. package/dist/src/workflow/gates/timeout-engine.d.ts +96 -0
  105. package/dist/src/workflow/gates/timeout-engine.d.ts.map +1 -0
  106. package/dist/src/workflow/gates/timeout-engine.js +162 -0
  107. package/dist/src/workflow/gates/timeout-engine.test.d.ts +2 -0
  108. package/dist/src/workflow/gates/timeout-engine.test.d.ts.map +1 -0
  109. package/dist/src/workflow/gates/timeout-engine.test.js +186 -0
  110. package/dist/src/workflow/gates/timer-gate.d.ts +125 -0
  111. package/dist/src/workflow/gates/timer-gate.d.ts.map +1 -0
  112. package/dist/src/workflow/gates/timer-gate.js +381 -0
  113. package/dist/src/workflow/gates/timer-gate.test.d.ts +2 -0
  114. package/dist/src/workflow/gates/timer-gate.test.d.ts.map +1 -0
  115. package/dist/src/workflow/gates/timer-gate.test.js +211 -0
  116. package/dist/src/workflow/gates/webhook-gate.d.ts +132 -0
  117. package/dist/src/workflow/gates/webhook-gate.d.ts.map +1 -0
  118. package/dist/src/workflow/gates/webhook-gate.js +216 -0
  119. package/dist/src/workflow/gates/webhook-gate.test.d.ts +2 -0
  120. package/dist/src/workflow/gates/webhook-gate.test.d.ts.map +1 -0
  121. package/dist/src/workflow/gates/webhook-gate.test.js +182 -0
  122. package/dist/src/workflow/index.d.ts +23 -2
  123. package/dist/src/workflow/index.d.ts.map +1 -1
  124. package/dist/src/workflow/index.js +15 -1
  125. package/dist/src/workflow/parallelism-executor.d.ts +25 -0
  126. package/dist/src/workflow/parallelism-executor.d.ts.map +1 -0
  127. package/dist/src/workflow/parallelism-executor.js +53 -0
  128. package/dist/src/workflow/parallelism-executor.test.d.ts +2 -0
  129. package/dist/src/workflow/parallelism-executor.test.d.ts.map +1 -0
  130. package/dist/src/workflow/parallelism-executor.test.js +191 -0
  131. package/dist/src/workflow/parallelism-types.d.ts +80 -0
  132. package/dist/src/workflow/parallelism-types.d.ts.map +1 -0
  133. package/dist/src/workflow/parallelism-types.js +8 -0
  134. package/dist/src/workflow/phase-context-injector.d.ts +29 -0
  135. package/dist/src/workflow/phase-context-injector.d.ts.map +1 -0
  136. package/dist/src/workflow/phase-context-injector.js +43 -0
  137. package/dist/src/workflow/phase-context-injector.test.d.ts +2 -0
  138. package/dist/src/workflow/phase-context-injector.test.d.ts.map +1 -0
  139. package/dist/src/workflow/phase-context-injector.test.js +123 -0
  140. package/dist/src/workflow/phase-output-collector.d.ts +39 -0
  141. package/dist/src/workflow/phase-output-collector.d.ts.map +1 -0
  142. package/dist/src/workflow/phase-output-collector.js +141 -0
  143. package/dist/src/workflow/phase-output-collector.test.d.ts +2 -0
  144. package/dist/src/workflow/phase-output-collector.test.d.ts.map +1 -0
  145. package/dist/src/workflow/phase-output-collector.test.js +179 -0
  146. package/dist/src/workflow/strategies/fan-in-strategy.d.ts +21 -0
  147. package/dist/src/workflow/strategies/fan-in-strategy.d.ts.map +1 -0
  148. package/dist/src/workflow/strategies/fan-in-strategy.js +92 -0
  149. package/dist/src/workflow/strategies/fan-in-strategy.test.d.ts +2 -0
  150. package/dist/src/workflow/strategies/fan-in-strategy.test.d.ts.map +1 -0
  151. package/dist/src/workflow/strategies/fan-in-strategy.test.js +182 -0
  152. package/dist/src/workflow/strategies/fan-out-strategy.d.ts +16 -0
  153. package/dist/src/workflow/strategies/fan-out-strategy.d.ts.map +1 -0
  154. package/dist/src/workflow/strategies/fan-out-strategy.js +47 -0
  155. package/dist/src/workflow/strategies/fan-out-strategy.test.d.ts +2 -0
  156. package/dist/src/workflow/strategies/fan-out-strategy.test.d.ts.map +1 -0
  157. package/dist/src/workflow/strategies/fan-out-strategy.test.js +97 -0
  158. package/dist/src/workflow/strategies/index.d.ts +4 -0
  159. package/dist/src/workflow/strategies/index.d.ts.map +1 -0
  160. package/dist/src/workflow/strategies/index.js +3 -0
  161. package/dist/src/workflow/strategies/race-strategy.d.ts +19 -0
  162. package/dist/src/workflow/strategies/race-strategy.d.ts.map +1 -0
  163. package/dist/src/workflow/strategies/race-strategy.js +92 -0
  164. package/dist/src/workflow/strategies/race-strategy.test.d.ts +2 -0
  165. package/dist/src/workflow/strategies/race-strategy.test.d.ts.map +1 -0
  166. package/dist/src/workflow/strategies/race-strategy.test.js +318 -0
  167. package/dist/src/workflow/transition-engine.d.ts.map +1 -1
  168. package/dist/src/workflow/transition-engine.js +12 -0
  169. package/dist/src/workflow/transition-engine.test.js +92 -0
  170. package/dist/src/workflow/workflow-registry.d.ts +5 -1
  171. package/dist/src/workflow/workflow-registry.d.ts.map +1 -1
  172. package/dist/src/workflow/workflow-registry.js +8 -0
  173. package/dist/src/workflow/workflow-registry.test.js +54 -0
  174. package/dist/src/workflow/workflow-types.d.ts +151 -6
  175. package/dist/src/workflow/workflow-types.d.ts.map +1 -1
  176. package/dist/src/workflow/workflow-types.js +71 -1
  177. package/dist/src/workflow/workflow-types.test.js +293 -2
  178. package/package.json +2 -2
@@ -0,0 +1,251 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { parseDuration, InMemoryGateStorage, activateGate, satisfyGate, timeoutGate, } from './gate-state.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 makeGateState(overrides = {}) {
15
+ return {
16
+ issueId: 'issue-1',
17
+ gateName: 'test-gate',
18
+ gateType: 'signal',
19
+ status: 'active',
20
+ activatedAt: Date.now(),
21
+ ...overrides,
22
+ };
23
+ }
24
+ // ---------------------------------------------------------------------------
25
+ // parseDuration
26
+ // ---------------------------------------------------------------------------
27
+ describe('parseDuration', () => {
28
+ it('parses hours: "4h" → 14400000ms', () => {
29
+ expect(parseDuration('4h')).toBe(14_400_000);
30
+ });
31
+ it('parses minutes: "30m" → 1800000ms', () => {
32
+ expect(parseDuration('30m')).toBe(1_800_000);
33
+ });
34
+ it('parses days: "2d" → 172800000ms', () => {
35
+ expect(parseDuration('2d')).toBe(172_800_000);
36
+ });
37
+ it('parses seconds: "15s" → 15000ms', () => {
38
+ expect(parseDuration('15s')).toBe(15_000);
39
+ });
40
+ it('throws on invalid format (missing unit)', () => {
41
+ expect(() => parseDuration('100')).toThrow('Invalid duration format');
42
+ });
43
+ it('throws on invalid format (unknown unit)', () => {
44
+ expect(() => parseDuration('5w')).toThrow('Invalid duration format');
45
+ });
46
+ it('throws on empty string', () => {
47
+ expect(() => parseDuration('')).toThrow('Invalid duration format');
48
+ });
49
+ it('throws on non-numeric value', () => {
50
+ expect(() => parseDuration('abch')).toThrow('Invalid duration format');
51
+ });
52
+ });
53
+ // ---------------------------------------------------------------------------
54
+ // InMemoryGateStorage
55
+ // ---------------------------------------------------------------------------
56
+ describe('InMemoryGateStorage', () => {
57
+ let storage;
58
+ beforeEach(() => {
59
+ storage = new InMemoryGateStorage();
60
+ });
61
+ describe('getGateState', () => {
62
+ it('returns null for non-existent gate', async () => {
63
+ const result = await storage.getGateState('issue-1', 'missing-gate');
64
+ expect(result).toBeNull();
65
+ });
66
+ it('returns stored gate state', async () => {
67
+ const state = makeGateState();
68
+ await storage.setGateState('issue-1', 'test-gate', state);
69
+ const result = await storage.getGateState('issue-1', 'test-gate');
70
+ expect(result).toEqual(state);
71
+ });
72
+ });
73
+ describe('setGateState', () => {
74
+ it('stores and retrieves gate state', async () => {
75
+ const state = makeGateState({ gateName: 'my-gate' });
76
+ await storage.setGateState('issue-1', 'my-gate', state);
77
+ const result = await storage.getGateState('issue-1', 'my-gate');
78
+ expect(result).toEqual(state);
79
+ });
80
+ it('overwrites existing gate state', async () => {
81
+ const state1 = makeGateState({ status: 'active' });
82
+ const state2 = makeGateState({ status: 'satisfied', satisfiedAt: Date.now() });
83
+ await storage.setGateState('issue-1', 'test-gate', state1);
84
+ await storage.setGateState('issue-1', 'test-gate', state2);
85
+ const result = await storage.getGateState('issue-1', 'test-gate');
86
+ expect(result?.status).toBe('satisfied');
87
+ });
88
+ });
89
+ describe('getActiveGates', () => {
90
+ it('returns empty array when no gates exist', async () => {
91
+ const result = await storage.getActiveGates('issue-1');
92
+ expect(result).toEqual([]);
93
+ });
94
+ it('returns only active gates for the given issue', async () => {
95
+ const active1 = makeGateState({ gateName: 'gate-a', status: 'active' });
96
+ const satisfied = makeGateState({ gateName: 'gate-b', status: 'satisfied' });
97
+ const active2 = makeGateState({ gateName: 'gate-c', status: 'active' });
98
+ await storage.setGateState('issue-1', 'gate-a', active1);
99
+ await storage.setGateState('issue-1', 'gate-b', satisfied);
100
+ await storage.setGateState('issue-1', 'gate-c', active2);
101
+ const result = await storage.getActiveGates('issue-1');
102
+ expect(result).toHaveLength(2);
103
+ expect(result.map(g => g.gateName)).toContain('gate-a');
104
+ expect(result.map(g => g.gateName)).toContain('gate-c');
105
+ });
106
+ it('does not return gates from other issues', async () => {
107
+ const state = makeGateState({ issueId: 'issue-2', gateName: 'gate-a', status: 'active' });
108
+ await storage.setGateState('issue-2', 'gate-a', state);
109
+ const result = await storage.getActiveGates('issue-1');
110
+ expect(result).toEqual([]);
111
+ });
112
+ });
113
+ describe('clearGateStates', () => {
114
+ it('clears all gates for an issue', async () => {
115
+ await storage.setGateState('issue-1', 'gate-a', makeGateState({ gateName: 'gate-a' }));
116
+ await storage.setGateState('issue-1', 'gate-b', makeGateState({ gateName: 'gate-b' }));
117
+ await storage.clearGateStates('issue-1');
118
+ expect(await storage.getGateState('issue-1', 'gate-a')).toBeNull();
119
+ expect(await storage.getGateState('issue-1', 'gate-b')).toBeNull();
120
+ });
121
+ it('does not affect other issues', async () => {
122
+ await storage.setGateState('issue-1', 'gate-a', makeGateState({ issueId: 'issue-1', gateName: 'gate-a' }));
123
+ await storage.setGateState('issue-2', 'gate-b', makeGateState({ issueId: 'issue-2', gateName: 'gate-b' }));
124
+ await storage.clearGateStates('issue-1');
125
+ expect(await storage.getGateState('issue-1', 'gate-a')).toBeNull();
126
+ expect(await storage.getGateState('issue-2', 'gate-b')).not.toBeNull();
127
+ });
128
+ });
129
+ });
130
+ // ---------------------------------------------------------------------------
131
+ // activateGate
132
+ // ---------------------------------------------------------------------------
133
+ describe('activateGate', () => {
134
+ let storage;
135
+ beforeEach(() => {
136
+ vi.useFakeTimers();
137
+ vi.setSystemTime(new Date('2025-06-01T12:00:00Z'));
138
+ storage = new InMemoryGateStorage();
139
+ });
140
+ afterEach(() => {
141
+ vi.useRealTimers();
142
+ });
143
+ it('creates an active gate state', async () => {
144
+ const gateDef = makeGateDefinition();
145
+ const state = await activateGate('issue-1', gateDef, storage);
146
+ expect(state.issueId).toBe('issue-1');
147
+ expect(state.gateName).toBe('test-gate');
148
+ expect(state.gateType).toBe('signal');
149
+ expect(state.status).toBe('active');
150
+ expect(state.activatedAt).toBe(Date.now());
151
+ });
152
+ it('computes timeout deadline from gate definition', async () => {
153
+ const gateDef = makeGateDefinition({
154
+ timeout: { duration: '4h', action: 'escalate' },
155
+ });
156
+ const state = await activateGate('issue-1', gateDef, storage);
157
+ expect(state.timeoutDeadline).toBe(Date.now() + 14_400_000);
158
+ expect(state.timeoutAction).toBe('escalate');
159
+ expect(state.timeoutDuration).toBe('4h');
160
+ });
161
+ it('does not set timeout fields when gate has no timeout', async () => {
162
+ const gateDef = makeGateDefinition();
163
+ const state = await activateGate('issue-1', gateDef, storage);
164
+ expect(state.timeoutDeadline).toBeUndefined();
165
+ expect(state.timeoutAction).toBeUndefined();
166
+ expect(state.timeoutDuration).toBeUndefined();
167
+ });
168
+ it('persists gate state in storage', async () => {
169
+ const gateDef = makeGateDefinition();
170
+ await activateGate('issue-1', gateDef, storage);
171
+ const stored = await storage.getGateState('issue-1', 'test-gate');
172
+ expect(stored).not.toBeNull();
173
+ expect(stored?.status).toBe('active');
174
+ });
175
+ });
176
+ // ---------------------------------------------------------------------------
177
+ // satisfyGate
178
+ // ---------------------------------------------------------------------------
179
+ describe('satisfyGate', () => {
180
+ let storage;
181
+ beforeEach(() => {
182
+ vi.useFakeTimers();
183
+ vi.setSystemTime(new Date('2025-06-01T12:00:00Z'));
184
+ storage = new InMemoryGateStorage();
185
+ });
186
+ afterEach(() => {
187
+ vi.useRealTimers();
188
+ });
189
+ it('marks an active gate as satisfied with source', async () => {
190
+ await storage.setGateState('issue-1', 'test-gate', makeGateState({ status: 'active' }));
191
+ const result = await satisfyGate('issue-1', 'test-gate', 'comment-123', storage);
192
+ expect(result).not.toBeNull();
193
+ expect(result?.status).toBe('satisfied');
194
+ expect(result?.signalSource).toBe('comment-123');
195
+ expect(result?.satisfiedAt).toBe(Date.now());
196
+ });
197
+ it('returns null for non-existent gate', async () => {
198
+ const result = await satisfyGate('issue-1', 'missing-gate', 'source', storage);
199
+ expect(result).toBeNull();
200
+ });
201
+ it('returns null for non-active gate', async () => {
202
+ await storage.setGateState('issue-1', 'test-gate', makeGateState({ status: 'satisfied' }));
203
+ const result = await satisfyGate('issue-1', 'test-gate', 'source', storage);
204
+ expect(result).toBeNull();
205
+ });
206
+ });
207
+ // ---------------------------------------------------------------------------
208
+ // timeoutGate
209
+ // ---------------------------------------------------------------------------
210
+ describe('timeoutGate', () => {
211
+ let storage;
212
+ beforeEach(() => {
213
+ vi.useFakeTimers();
214
+ vi.setSystemTime(new Date('2025-06-01T12:00:00Z'));
215
+ storage = new InMemoryGateStorage();
216
+ });
217
+ afterEach(() => {
218
+ vi.useRealTimers();
219
+ });
220
+ it('marks an active gate as timed-out', async () => {
221
+ await storage.setGateState('issue-1', 'test-gate', makeGateState({ status: 'active' }));
222
+ const result = await timeoutGate('issue-1', 'test-gate', storage);
223
+ expect(result).not.toBeNull();
224
+ expect(result?.status).toBe('timed-out');
225
+ expect(result?.timedOutAt).toBe(Date.now());
226
+ });
227
+ it('returns null for non-existent gate', async () => {
228
+ const result = await timeoutGate('issue-1', 'missing-gate', storage);
229
+ expect(result).toBeNull();
230
+ });
231
+ it('returns null for non-active gate', async () => {
232
+ await storage.setGateState('issue-1', 'test-gate', makeGateState({ status: 'timed-out' }));
233
+ const result = await timeoutGate('issue-1', 'test-gate', storage);
234
+ expect(result).toBeNull();
235
+ });
236
+ });
237
+ // ---------------------------------------------------------------------------
238
+ // getActiveGates (functional)
239
+ // ---------------------------------------------------------------------------
240
+ describe('getActiveGates returns only active gates', () => {
241
+ it('filters by status correctly', async () => {
242
+ const storage = new InMemoryGateStorage();
243
+ await storage.setGateState('issue-1', 'gate-active', makeGateState({ gateName: 'gate-active', status: 'active' }));
244
+ await storage.setGateState('issue-1', 'gate-satisfied', makeGateState({ gateName: 'gate-satisfied', status: 'satisfied' }));
245
+ await storage.setGateState('issue-1', 'gate-timed-out', makeGateState({ gateName: 'gate-timed-out', status: 'timed-out' }));
246
+ await storage.setGateState('issue-1', 'gate-pending', makeGateState({ gateName: 'gate-pending', status: 'pending' }));
247
+ const active = await storage.getActiveGates('issue-1');
248
+ expect(active).toHaveLength(1);
249
+ expect(active[0].gateName).toBe('gate-active');
250
+ });
251
+ });
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Gate Evaluator — Decision Engine Integration Layer
3
+ *
4
+ * Unified gate evaluation module that the decision engine calls to check
5
+ * whether workflow phase transitions are blocked by unsatisfied gates.
6
+ * Wires together signal gates, timer gates, webhook gates, and the timeout
7
+ * engine into a single evaluation pipeline.
8
+ *
9
+ * Main entry points:
10
+ * - evaluateGatesForPhase() — called by the governor before decideAction()
11
+ * - activateGatesForPhase() — called when entering a new workflow phase
12
+ * - clearGatesForIssue() — called when an issue reaches terminal state
13
+ */
14
+ import type { GateState, GateStorage } from '../gate-state.js';
15
+ import type { WorkflowDefinition, GateDefinition } from '../workflow-types.js';
16
+ import type { TimeoutResolution } from './timeout-engine.js';
17
+ /**
18
+ * Options for evaluating gates applicable to a workflow phase.
19
+ * The governor populates this before calling the decision engine.
20
+ */
21
+ export interface GateEvaluationOptions {
22
+ /** The issue identifier */
23
+ issueId: string;
24
+ /** The current workflow phase name */
25
+ phase: string;
26
+ /** The workflow definition containing gate configurations */
27
+ workflow: WorkflowDefinition;
28
+ /** Storage adapter for gate state persistence */
29
+ storage: GateStorage;
30
+ /** For signal gates — latest comments to check */
31
+ comments?: Array<{
32
+ text: string;
33
+ isBot: boolean;
34
+ }>;
35
+ /** Current time for timer/timeout evaluation (epoch ms, defaults to Date.now()) */
36
+ now?: number;
37
+ }
38
+ /**
39
+ * Result of evaluating all gates for a workflow phase.
40
+ * Consumed by the decision engine to determine if transitions are blocked.
41
+ */
42
+ export interface GateEvaluationResult {
43
+ /** Whether all gates for this phase are satisfied (or no gates apply) */
44
+ allSatisfied: boolean;
45
+ /** Active unsatisfied gates */
46
+ activeGates: GateState[];
47
+ /** Gates that were satisfied during this evaluation */
48
+ newlySatisfied: GateState[];
49
+ /** Timeout resolutions that need to be acted on */
50
+ timeoutResolutions: TimeoutResolution[];
51
+ /** Reason string for decision engine logging */
52
+ reason: string;
53
+ }
54
+ /**
55
+ * Get all gate definitions from a workflow that apply to a given phase,
56
+ * across all gate types (signal, timer, webhook).
57
+ *
58
+ * @param workflow - The workflow definition containing gate configurations
59
+ * @param phase - The phase name to filter by
60
+ * @returns Array of GateDefinition objects applicable to the phase
61
+ */
62
+ export declare function getApplicableGates(workflow: WorkflowDefinition, phase: string): GateDefinition[];
63
+ /**
64
+ * Evaluate all gates applicable to a workflow phase.
65
+ *
66
+ * This is the main entry point called by the governor before invoking
67
+ * the decision engine. It evaluates each active gate according to its
68
+ * type and returns an aggregated result indicating whether all gates
69
+ * are satisfied.
70
+ *
71
+ * Evaluation pipeline:
72
+ * 1. Get applicable gates for the phase from the workflow definition
73
+ * 2. For each applicable gate, retrieve its persisted state from storage
74
+ * 3. For each active gate:
75
+ * - Signal gates: check each comment via evaluateSignalGate(); satisfy on match
76
+ * - Timer gates: check via evaluateTimerGate(); satisfy if fired
77
+ * - Webhook gates: check via evaluateWebhookGate() (satisfaction is external)
78
+ * 4. Check timeouts via processGateTimeouts()
79
+ * 5. Return aggregated result
80
+ *
81
+ * @param opts - Gate evaluation options
82
+ * @returns Aggregated gate evaluation result
83
+ */
84
+ export declare function evaluateGatesForPhase(opts: GateEvaluationOptions): Promise<GateEvaluationResult>;
85
+ /**
86
+ * Activate all gates applicable to a workflow phase.
87
+ *
88
+ * Called when a workflow transitions to a new phase. Finds all gate
89
+ * definitions that apply to the phase and activates each one via
90
+ * the gate-state module, creating persisted gate state with computed
91
+ * timeout deadlines.
92
+ *
93
+ * Gates that are already activated (i.e., have existing state) are
94
+ * skipped to prevent re-activation on subsequent poll cycles.
95
+ *
96
+ * For webhook gates, a cryptographic token is generated and stored in
97
+ * the gate state. The callback URL is stored in `signalSource` so
98
+ * external systems can discover it.
99
+ *
100
+ * @param issueId - The issue identifier
101
+ * @param phase - The phase being entered
102
+ * @param workflow - The workflow definition containing gate configurations
103
+ * @param storage - The gate storage adapter for persisting state
104
+ * @param baseUrl - Optional base URL for webhook callback URLs
105
+ * @returns Array of newly activated gate states
106
+ */
107
+ export declare function activateGatesForPhase(issueId: string, phase: string, workflow: WorkflowDefinition, storage: GateStorage, baseUrl?: string): Promise<GateState[]>;
108
+ /**
109
+ * Clear all gate states for an issue.
110
+ *
111
+ * Called when an issue reaches a terminal state (Accepted, Canceled,
112
+ * Duplicate) to clean up any persisted gate state. Delegates directly
113
+ * to the storage adapter's clearGateStates() method.
114
+ *
115
+ * @param issueId - The issue identifier
116
+ * @param storage - The gate storage adapter
117
+ */
118
+ export declare function clearGatesForIssue(issueId: string, storage: GateStorage): Promise<void>;
119
+ //# sourceMappingURL=gate-evaluator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gate-evaluator.d.ts","sourceRoot":"","sources":["../../../../src/workflow/gates/gate-evaluator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAE9D,OAAO,KAAK,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAO9E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAqB5D;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACpC,2BAA2B;IAC3B,OAAO,EAAE,MAAM,CAAA;IACf,sCAAsC;IACtC,KAAK,EAAE,MAAM,CAAA;IACb,6DAA6D;IAC7D,QAAQ,EAAE,kBAAkB,CAAA;IAC5B,iDAAiD;IACjD,OAAO,EAAE,WAAW,CAAA;IACpB,kDAAkD;IAClD,QAAQ,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,CAAC,CAAA;IAClD,mFAAmF;IACnF,GAAG,CAAC,EAAE,MAAM,CAAA;CACb;AAED;;;GAGG;AACH,MAAM,WAAW,oBAAoB;IACnC,yEAAyE;IACzE,YAAY,EAAE,OAAO,CAAA;IACrB,+BAA+B;IAC/B,WAAW,EAAE,SAAS,EAAE,CAAA;IACxB,uDAAuD;IACvD,cAAc,EAAE,SAAS,EAAE,CAAA;IAC3B,mDAAmD;IACnD,kBAAkB,EAAE,iBAAiB,EAAE,CAAA;IACvC,gDAAgD;IAChD,MAAM,EAAE,MAAM,CAAA;CACf;AAMD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,kBAAkB,EAAE,KAAK,EAAE,MAAM,GAAG,cAAc,EAAE,CAMhG;AAMD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,qBAAqB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CA2HtG;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,kBAAkB,EAC5B,OAAO,EAAE,WAAW,EACpB,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,SAAS,EAAE,CAAC,CAwCtB;AAED;;;;;;;;;GASG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,IAAI,CAAC,CAGf"}
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Gate Evaluator — Decision Engine Integration Layer
3
+ *
4
+ * Unified gate evaluation module that the decision engine calls to check
5
+ * whether workflow phase transitions are blocked by unsatisfied gates.
6
+ * Wires together signal gates, timer gates, webhook gates, and the timeout
7
+ * engine into a single evaluation pipeline.
8
+ *
9
+ * Main entry points:
10
+ * - evaluateGatesForPhase() — called by the governor before decideAction()
11
+ * - activateGatesForPhase() — called when entering a new workflow phase
12
+ * - clearGatesForIssue() — called when an issue reaches terminal state
13
+ */
14
+ import { activateGate, satisfyGate } from '../gate-state.js';
15
+ import { evaluateSignalGate } from './signal-gate.js';
16
+ import { getApplicableSignalGates } from './signal-gate.js';
17
+ import { evaluateTimerGate } from './timer-gate.js';
18
+ import { getApplicableTimerGates } from './timer-gate.js';
19
+ import { evaluateWebhookGate, generateWebhookToken, buildCallbackUrl } from './webhook-gate.js';
20
+ import { getApplicableWebhookGates } from './webhook-gate.js';
21
+ import { processGateTimeouts } from './timeout-engine.js';
22
+ // ============================================
23
+ // Logger
24
+ // ============================================
25
+ const log = {
26
+ info: (msg, data) => console.log(`[gate-evaluator] ${msg}`, data ? JSON.stringify(data) : ''),
27
+ warn: (msg, data) => console.warn(`[gate-evaluator] ${msg}`, data ? JSON.stringify(data) : ''),
28
+ error: (msg, data) => console.error(`[gate-evaluator] ${msg}`, data ? JSON.stringify(data) : ''),
29
+ debug: (_msg, _data) => { },
30
+ };
31
+ // ============================================
32
+ // Gate Query Helpers
33
+ // ============================================
34
+ /**
35
+ * Get all gate definitions from a workflow that apply to a given phase,
36
+ * across all gate types (signal, timer, webhook).
37
+ *
38
+ * @param workflow - The workflow definition containing gate configurations
39
+ * @param phase - The phase name to filter by
40
+ * @returns Array of GateDefinition objects applicable to the phase
41
+ */
42
+ export function getApplicableGates(workflow, phase) {
43
+ return [
44
+ ...getApplicableSignalGates(workflow, phase),
45
+ ...getApplicableTimerGates(workflow, phase),
46
+ ...getApplicableWebhookGates(workflow, phase),
47
+ ];
48
+ }
49
+ // ============================================
50
+ // Main Entry Points
51
+ // ============================================
52
+ /**
53
+ * Evaluate all gates applicable to a workflow phase.
54
+ *
55
+ * This is the main entry point called by the governor before invoking
56
+ * the decision engine. It evaluates each active gate according to its
57
+ * type and returns an aggregated result indicating whether all gates
58
+ * are satisfied.
59
+ *
60
+ * Evaluation pipeline:
61
+ * 1. Get applicable gates for the phase from the workflow definition
62
+ * 2. For each applicable gate, retrieve its persisted state from storage
63
+ * 3. For each active gate:
64
+ * - Signal gates: check each comment via evaluateSignalGate(); satisfy on match
65
+ * - Timer gates: check via evaluateTimerGate(); satisfy if fired
66
+ * - Webhook gates: check via evaluateWebhookGate() (satisfaction is external)
67
+ * 4. Check timeouts via processGateTimeouts()
68
+ * 5. Return aggregated result
69
+ *
70
+ * @param opts - Gate evaluation options
71
+ * @returns Aggregated gate evaluation result
72
+ */
73
+ export async function evaluateGatesForPhase(opts) {
74
+ const { issueId, phase, workflow, storage, comments, now } = opts;
75
+ const currentTime = now ?? Date.now();
76
+ // 1. Get applicable gate definitions for this phase
77
+ const applicableGateDefs = getApplicableGates(workflow, phase);
78
+ // If no gates apply, everything is satisfied
79
+ if (applicableGateDefs.length === 0) {
80
+ return {
81
+ allSatisfied: true,
82
+ activeGates: [],
83
+ newlySatisfied: [],
84
+ timeoutResolutions: [],
85
+ reason: `No gates apply to phase "${phase}"`,
86
+ };
87
+ }
88
+ const newlySatisfied = [];
89
+ // 2. For each applicable gate, check if it's already activated
90
+ for (const gateDef of applicableGateDefs) {
91
+ const gateState = await storage.getGateState(issueId, gateDef.name);
92
+ // Gate not yet activated — skip evaluation (it will be activated
93
+ // when the phase is entered via activateGatesForPhase)
94
+ if (!gateState || gateState.status !== 'active') {
95
+ continue;
96
+ }
97
+ // 3. Evaluate based on gate type
98
+ switch (gateDef.type) {
99
+ case 'signal': {
100
+ // Check each comment against the signal gate
101
+ if (comments && comments.length > 0) {
102
+ for (const comment of comments) {
103
+ const result = evaluateSignalGate(gateDef, comment.text, comment.isBot);
104
+ if (result.matched) {
105
+ const satisfied = await satisfyGate(issueId, gateDef.name, result.source ?? 'signal-match', storage);
106
+ if (satisfied) {
107
+ newlySatisfied.push(satisfied);
108
+ log.info('Signal gate satisfied by comment', {
109
+ issueId,
110
+ gateName: gateDef.name,
111
+ source: result.source,
112
+ });
113
+ }
114
+ break; // Gate satisfied, no need to check more comments
115
+ }
116
+ }
117
+ }
118
+ break;
119
+ }
120
+ case 'timer': {
121
+ const result = evaluateTimerGate(gateDef, currentTime);
122
+ if (result.fired) {
123
+ const satisfied = await satisfyGate(issueId, gateDef.name, `timer-fired:${result.nextFireTime}`, storage);
124
+ if (satisfied) {
125
+ newlySatisfied.push(satisfied);
126
+ log.info('Timer gate fired', {
127
+ issueId,
128
+ gateName: gateDef.name,
129
+ nextFireTime: result.nextFireTime,
130
+ });
131
+ }
132
+ }
133
+ break;
134
+ }
135
+ case 'webhook': {
136
+ // Webhook gates are satisfied externally via HTTP callback.
137
+ // We just evaluate the current state to check for timeout.
138
+ evaluateWebhookGate(gateDef, gateState);
139
+ break;
140
+ }
141
+ }
142
+ }
143
+ // 4. Check timeouts on all active gates
144
+ const activeGates = await storage.getActiveGates(issueId);
145
+ const timeoutResolutions = await processGateTimeouts(activeGates, storage, currentTime);
146
+ // 5. Re-fetch active gates after timeout processing (some may have been timed-out)
147
+ const remainingActiveGates = await storage.getActiveGates(issueId);
148
+ // Filter to only those gates that are relevant to this phase
149
+ const applicableGateNames = new Set(applicableGateDefs.map(g => g.name));
150
+ const phaseActiveGates = remainingActiveGates.filter(g => applicableGateNames.has(g.gateName));
151
+ const allSatisfied = phaseActiveGates.length === 0;
152
+ // Build reason string
153
+ let reason;
154
+ if (allSatisfied) {
155
+ if (newlySatisfied.length > 0) {
156
+ const names = newlySatisfied.map(g => g.gateName).join(', ');
157
+ reason = `All gates satisfied for phase "${phase}" (newly satisfied: ${names})`;
158
+ }
159
+ else {
160
+ reason = `All gates satisfied for phase "${phase}"`;
161
+ }
162
+ }
163
+ else {
164
+ const activeNames = phaseActiveGates.map(g => g.gateName).join(', ');
165
+ reason = `Unsatisfied gates for phase "${phase}": ${activeNames}`;
166
+ }
167
+ return {
168
+ allSatisfied,
169
+ activeGates: phaseActiveGates,
170
+ newlySatisfied,
171
+ timeoutResolutions,
172
+ reason,
173
+ };
174
+ }
175
+ /**
176
+ * Activate all gates applicable to a workflow phase.
177
+ *
178
+ * Called when a workflow transitions to a new phase. Finds all gate
179
+ * definitions that apply to the phase and activates each one via
180
+ * the gate-state module, creating persisted gate state with computed
181
+ * timeout deadlines.
182
+ *
183
+ * Gates that are already activated (i.e., have existing state) are
184
+ * skipped to prevent re-activation on subsequent poll cycles.
185
+ *
186
+ * For webhook gates, a cryptographic token is generated and stored in
187
+ * the gate state. The callback URL is stored in `signalSource` so
188
+ * external systems can discover it.
189
+ *
190
+ * @param issueId - The issue identifier
191
+ * @param phase - The phase being entered
192
+ * @param workflow - The workflow definition containing gate configurations
193
+ * @param storage - The gate storage adapter for persisting state
194
+ * @param baseUrl - Optional base URL for webhook callback URLs
195
+ * @returns Array of newly activated gate states
196
+ */
197
+ export async function activateGatesForPhase(issueId, phase, workflow, storage, baseUrl) {
198
+ const applicableGateDefs = getApplicableGates(workflow, phase);
199
+ if (applicableGateDefs.length === 0) {
200
+ log.debug('No gates to activate for phase', { issueId, phase });
201
+ return [];
202
+ }
203
+ const activated = [];
204
+ for (const gateDef of applicableGateDefs) {
205
+ // Skip if already activated (prevent re-activation on repeated polls)
206
+ const existing = await storage.getGateState(issueId, gateDef.name);
207
+ if (existing) {
208
+ log.debug('Gate already activated, skipping', { issueId, gateName: gateDef.name, status: existing.status });
209
+ continue;
210
+ }
211
+ const gateState = await activateGate(issueId, gateDef, storage);
212
+ // For webhook gates, generate and persist a token + callback URL
213
+ if (gateDef.type === 'webhook' && baseUrl) {
214
+ const token = generateWebhookToken();
215
+ const callbackUrl = buildCallbackUrl(baseUrl, issueId, gateDef.name, token);
216
+ gateState.webhookToken = token;
217
+ gateState.signalSource = callbackUrl;
218
+ await storage.setGateState(issueId, gateDef.name, gateState);
219
+ }
220
+ activated.push(gateState);
221
+ log.info('Gate activated for phase', {
222
+ issueId,
223
+ phase,
224
+ gateName: gateDef.name,
225
+ gateType: gateDef.type,
226
+ });
227
+ }
228
+ return activated;
229
+ }
230
+ /**
231
+ * Clear all gate states for an issue.
232
+ *
233
+ * Called when an issue reaches a terminal state (Accepted, Canceled,
234
+ * Duplicate) to clean up any persisted gate state. Delegates directly
235
+ * to the storage adapter's clearGateStates() method.
236
+ *
237
+ * @param issueId - The issue identifier
238
+ * @param storage - The gate storage adapter
239
+ */
240
+ export async function clearGatesForIssue(issueId, storage) {
241
+ await storage.clearGateStates(issueId);
242
+ log.info('Cleared all gate states for issue', { issueId });
243
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=gate-evaluator.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gate-evaluator.test.d.ts","sourceRoot":"","sources":["../../../../src/workflow/gates/gate-evaluator.test.ts"],"names":[],"mappings":""}