@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,240 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { evaluateGatesForPhase, activateGatesForPhase, clearGatesForIssue, getApplicableGates, } from './gate-evaluator.js';
3
+ import { InMemoryGateStorage } from '../gate-state.js';
4
+ // ---------------------------------------------------------------------------
5
+ // Helpers
6
+ // ---------------------------------------------------------------------------
7
+ function makeGateDefinition(overrides = {}) {
8
+ return {
9
+ name: 'test-gate',
10
+ type: 'signal',
11
+ trigger: { source: 'comment', match: 'APPROVE' },
12
+ ...overrides,
13
+ };
14
+ }
15
+ function makeWorkflow(gates = []) {
16
+ return {
17
+ apiVersion: 'v1.1',
18
+ kind: 'WorkflowDefinition',
19
+ metadata: { name: 'test-workflow' },
20
+ phases: [{ name: 'development', template: 'dev' }],
21
+ transitions: [{ from: 'Backlog', to: 'development' }],
22
+ gates,
23
+ };
24
+ }
25
+ // ---------------------------------------------------------------------------
26
+ // evaluateGatesForPhase
27
+ // ---------------------------------------------------------------------------
28
+ describe('evaluateGatesForPhase', () => {
29
+ let storage;
30
+ beforeEach(() => {
31
+ vi.useFakeTimers();
32
+ vi.setSystemTime(new Date('2025-06-01T12:00:00Z'));
33
+ storage = new InMemoryGateStorage();
34
+ });
35
+ afterEach(() => {
36
+ vi.useRealTimers();
37
+ });
38
+ it('returns all satisfied when no gates apply', async () => {
39
+ const workflow = makeWorkflow([]);
40
+ const result = await evaluateGatesForPhase({
41
+ issueId: 'issue-1',
42
+ phase: 'development',
43
+ workflow,
44
+ storage,
45
+ });
46
+ expect(result.allSatisfied).toBe(true);
47
+ expect(result.activeGates).toEqual([]);
48
+ expect(result.newlySatisfied).toEqual([]);
49
+ expect(result.timeoutResolutions).toEqual([]);
50
+ expect(result.reason).toContain('No gates apply');
51
+ });
52
+ it('satisfies active signal gate when a matching comment is provided', async () => {
53
+ const gate = makeGateDefinition({
54
+ name: 'approval',
55
+ type: 'signal',
56
+ trigger: { source: 'comment', match: 'APPROVE' },
57
+ appliesTo: ['development'],
58
+ });
59
+ const workflow = makeWorkflow([gate]);
60
+ // Activate the gate first
61
+ await activateGatesForPhase('issue-1', 'development', workflow, storage);
62
+ // Evaluate with a matching comment
63
+ const result = await evaluateGatesForPhase({
64
+ issueId: 'issue-1',
65
+ phase: 'development',
66
+ workflow,
67
+ storage,
68
+ comments: [{ text: 'APPROVE', isBot: false }],
69
+ });
70
+ expect(result.allSatisfied).toBe(true);
71
+ expect(result.newlySatisfied).toHaveLength(1);
72
+ expect(result.newlySatisfied[0].gateName).toBe('approval');
73
+ });
74
+ it('timer gate fires at correct time', async () => {
75
+ const gate = makeGateDefinition({
76
+ name: 'scheduled',
77
+ type: 'timer',
78
+ trigger: { cron: '0 12 * * *' }, // fires at 12:00
79
+ appliesTo: ['development'],
80
+ });
81
+ const workflow = makeWorkflow([gate]);
82
+ // Activate the gate
83
+ await activateGatesForPhase('issue-1', 'development', workflow, storage);
84
+ // Evaluate at 12:00:30 (should fire since current minute matches)
85
+ const now = new Date(2025, 5, 1, 12, 0, 30, 0).getTime();
86
+ const result = await evaluateGatesForPhase({
87
+ issueId: 'issue-1',
88
+ phase: 'development',
89
+ workflow,
90
+ storage,
91
+ now,
92
+ });
93
+ expect(result.allSatisfied).toBe(true);
94
+ expect(result.newlySatisfied).toHaveLength(1);
95
+ expect(result.newlySatisfied[0].gateName).toBe('scheduled');
96
+ });
97
+ it('unsatisfied gates block (allSatisfied is false)', async () => {
98
+ const gate = makeGateDefinition({
99
+ name: 'approval',
100
+ type: 'signal',
101
+ trigger: { source: 'comment', match: 'APPROVE' },
102
+ appliesTo: ['development'],
103
+ });
104
+ const workflow = makeWorkflow([gate]);
105
+ // Activate the gate
106
+ await activateGatesForPhase('issue-1', 'development', workflow, storage);
107
+ // Evaluate without matching comments
108
+ const result = await evaluateGatesForPhase({
109
+ issueId: 'issue-1',
110
+ phase: 'development',
111
+ workflow,
112
+ storage,
113
+ comments: [{ text: 'not a match', isBot: false }],
114
+ });
115
+ expect(result.allSatisfied).toBe(false);
116
+ expect(result.activeGates).toHaveLength(1);
117
+ expect(result.activeGates[0].gateName).toBe('approval');
118
+ expect(result.reason).toContain('Unsatisfied gates');
119
+ });
120
+ it('processes timeouts for active gates', async () => {
121
+ const gate = makeGateDefinition({
122
+ name: 'timed-gate',
123
+ type: 'signal',
124
+ trigger: { source: 'comment', match: 'APPROVE' },
125
+ timeout: { duration: '1h', action: 'fail' },
126
+ appliesTo: ['development'],
127
+ });
128
+ const workflow = makeWorkflow([gate]);
129
+ // Activate the gate
130
+ await activateGatesForPhase('issue-1', 'development', workflow, storage);
131
+ // Advance time past the timeout
132
+ vi.advanceTimersByTime(2 * 60 * 60 * 1000); // 2 hours
133
+ const result = await evaluateGatesForPhase({
134
+ issueId: 'issue-1',
135
+ phase: 'development',
136
+ workflow,
137
+ storage,
138
+ });
139
+ expect(result.timeoutResolutions).toHaveLength(1);
140
+ expect(result.timeoutResolutions[0].type).toBe('fail');
141
+ expect(result.timeoutResolutions[0].gateName).toBe('timed-gate');
142
+ });
143
+ });
144
+ // ---------------------------------------------------------------------------
145
+ // activateGatesForPhase
146
+ // ---------------------------------------------------------------------------
147
+ describe('activateGatesForPhase', () => {
148
+ let storage;
149
+ beforeEach(() => {
150
+ vi.useFakeTimers();
151
+ vi.setSystemTime(new Date('2025-06-01T12:00:00Z'));
152
+ storage = new InMemoryGateStorage();
153
+ });
154
+ afterEach(() => {
155
+ vi.useRealTimers();
156
+ });
157
+ it('activates applicable gates', async () => {
158
+ const gates = [
159
+ makeGateDefinition({ name: 'gate-1', type: 'signal', appliesTo: ['development'] }),
160
+ makeGateDefinition({ name: 'gate-2', type: 'timer', trigger: { cron: '0 9 * * *' }, appliesTo: ['development'] }),
161
+ ];
162
+ const workflow = makeWorkflow(gates);
163
+ const activated = await activateGatesForPhase('issue-1', 'development', workflow, storage);
164
+ expect(activated).toHaveLength(2);
165
+ expect(activated.map(g => g.gateName)).toContain('gate-1');
166
+ expect(activated.map(g => g.gateName)).toContain('gate-2');
167
+ });
168
+ it('skips already-activated gates', async () => {
169
+ const gate = makeGateDefinition({ name: 'gate-1', type: 'signal', appliesTo: ['development'] });
170
+ const workflow = makeWorkflow([gate]);
171
+ // Activate once
172
+ const first = await activateGatesForPhase('issue-1', 'development', workflow, storage);
173
+ expect(first).toHaveLength(1);
174
+ // Activate again — should skip
175
+ const second = await activateGatesForPhase('issue-1', 'development', workflow, storage);
176
+ expect(second).toHaveLength(0);
177
+ });
178
+ it('returns empty array when no gates apply', async () => {
179
+ const workflow = makeWorkflow([]);
180
+ const activated = await activateGatesForPhase('issue-1', 'development', workflow, storage);
181
+ expect(activated).toEqual([]);
182
+ });
183
+ it('does not activate gates for other phases', async () => {
184
+ const gate = makeGateDefinition({ name: 'qa-gate', type: 'signal', appliesTo: ['qa'] });
185
+ const workflow = makeWorkflow([gate]);
186
+ const activated = await activateGatesForPhase('issue-1', 'development', workflow, storage);
187
+ expect(activated).toEqual([]);
188
+ });
189
+ });
190
+ // ---------------------------------------------------------------------------
191
+ // clearGatesForIssue
192
+ // ---------------------------------------------------------------------------
193
+ describe('clearGatesForIssue', () => {
194
+ it('delegates to storage.clearGateStates', async () => {
195
+ const storage = new InMemoryGateStorage();
196
+ // Add some gates
197
+ await storage.setGateState('issue-1', 'gate-a', {
198
+ issueId: 'issue-1',
199
+ gateName: 'gate-a',
200
+ gateType: 'signal',
201
+ status: 'active',
202
+ activatedAt: Date.now(),
203
+ });
204
+ await clearGatesForIssue('issue-1', storage);
205
+ const result = await storage.getGateState('issue-1', 'gate-a');
206
+ expect(result).toBeNull();
207
+ });
208
+ });
209
+ // ---------------------------------------------------------------------------
210
+ // getApplicableGates
211
+ // ---------------------------------------------------------------------------
212
+ describe('getApplicableGates', () => {
213
+ it('aggregates gates across all types', () => {
214
+ const gates = [
215
+ makeGateDefinition({ name: 'signal-1', type: 'signal', appliesTo: ['development'] }),
216
+ makeGateDefinition({ name: 'timer-1', type: 'timer', trigger: { cron: '0 9 * * *' }, appliesTo: ['development'] }),
217
+ makeGateDefinition({ name: 'webhook-1', type: 'webhook', trigger: { endpoint: '/api' }, appliesTo: ['development'] }),
218
+ ];
219
+ const workflow = makeWorkflow(gates);
220
+ const result = getApplicableGates(workflow, 'development');
221
+ expect(result).toHaveLength(3);
222
+ expect(result.map(g => g.name)).toContain('signal-1');
223
+ expect(result.map(g => g.name)).toContain('timer-1');
224
+ expect(result.map(g => g.name)).toContain('webhook-1');
225
+ });
226
+ it('returns empty array when no gates match phase', () => {
227
+ const gates = [
228
+ makeGateDefinition({ name: 'qa-gate', type: 'signal', appliesTo: ['qa'] }),
229
+ ];
230
+ const workflow = makeWorkflow(gates);
231
+ const result = getApplicableGates(workflow, 'development');
232
+ expect(result).toEqual([]);
233
+ });
234
+ it('returns empty array when workflow has no gates', () => {
235
+ const workflow = makeWorkflow();
236
+ delete workflow.gates;
237
+ const result = getApplicableGates(workflow, 'development');
238
+ expect(result).toEqual([]);
239
+ });
240
+ });
@@ -0,0 +1,114 @@
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
+ import type { GateDefinition, WorkflowDefinition } from '../workflow-types.js';
13
+ /**
14
+ * Trigger configuration for a signal gate.
15
+ * Defines how incoming comments are matched against the gate condition.
16
+ */
17
+ export interface SignalGateTrigger {
18
+ /** Whether to match against the full comment text or the first-line directive only */
19
+ source: 'comment' | 'directive';
20
+ /** String to match — can be an exact string or a regex pattern */
21
+ match: string;
22
+ }
23
+ /**
24
+ * Result of evaluating a comment against a signal gate
25
+ */
26
+ export interface SignalGateResult {
27
+ /** Whether the comment matched the signal gate trigger */
28
+ matched: boolean;
29
+ /** The content that matched (the full comment or directive line) */
30
+ source?: string;
31
+ }
32
+ /**
33
+ * Type guard to validate that a trigger object has the correct shape
34
+ * for a signal gate trigger.
35
+ *
36
+ * A valid SignalGateTrigger must have:
37
+ * - `source`: either 'comment' or 'directive'
38
+ * - `match`: a non-empty string
39
+ *
40
+ * @param trigger - The trigger record to validate
41
+ * @returns True if the trigger is a valid SignalGateTrigger
42
+ */
43
+ export declare function isSignalGateTrigger(trigger: Record<string, unknown>): trigger is Record<string, unknown> & SignalGateTrigger;
44
+ /**
45
+ * Evaluate whether a comment satisfies a signal gate trigger.
46
+ *
47
+ * This is a pure function (no I/O) that checks if a comment matches
48
+ * the signal gate's trigger configuration.
49
+ *
50
+ * Matching rules:
51
+ * - Bot comments are always skipped (returns { matched: false })
52
+ * - When `trigger.source` is 'directive', only the first non-empty line
53
+ * of the comment is checked (consistent with override-parser.ts)
54
+ * - When `trigger.source` is 'comment', the full trimmed comment text is checked
55
+ * - The `trigger.match` string is first tried as an exact case-insensitive match,
56
+ * then as a regex pattern (case-insensitive)
57
+ *
58
+ * @param gate - The gate definition containing a signal trigger
59
+ * @param comment - The full comment body text
60
+ * @param isBot - Whether the comment was authored by a bot
61
+ * @returns A SignalGateResult indicating whether the comment matched
62
+ */
63
+ export declare function evaluateSignalGate(gate: GateDefinition, comment: string, isBot: boolean): SignalGateResult;
64
+ /**
65
+ * Get all signal gates from a workflow definition that apply to a given phase.
66
+ *
67
+ * Filters gates by:
68
+ * 1. `type === 'signal'`
69
+ * 2. `appliesTo` includes the given phase name, OR `appliesTo` is not defined
70
+ * (gate applies to all phases)
71
+ *
72
+ * @param workflow - The workflow definition containing gate configurations
73
+ * @param phase - The phase name to filter by
74
+ * @returns Array of GateDefinition objects that are signal gates applicable to the phase
75
+ */
76
+ export declare function getApplicableSignalGates(workflow: WorkflowDefinition, phase: string): GateDefinition[];
77
+ /**
78
+ * Name used for the implicit HOLD gate created for backward compatibility
79
+ */
80
+ export declare const IMPLICIT_HOLD_GATE_NAME = "__implicit-hold";
81
+ /**
82
+ * Name used for the implicit RESUME gate created for backward compatibility
83
+ */
84
+ export declare const IMPLICIT_RESUME_GATE_NAME = "__implicit-resume";
85
+ /**
86
+ * Create an implicit signal gate definition that matches the HOLD directive.
87
+ *
88
+ * This provides backward compatibility with the existing HOLD/RESUME system.
89
+ * When a HOLD directive is detected and no explicit signal gate is defined,
90
+ * this creates a gate that will pause the workflow until a RESUME directive
91
+ * is received.
92
+ *
93
+ * The created gate matches:
94
+ * - Directive source: first line of comment
95
+ * - Pattern: `^hold(?:\s*[---]\s*(.+))?$` (matches HOLD or HOLD -- reason)
96
+ *
97
+ * @returns A GateDefinition representing the implicit HOLD gate
98
+ */
99
+ export declare function createImplicitHoldGate(): GateDefinition;
100
+ /**
101
+ * Create an implicit signal gate definition that matches the RESUME directive.
102
+ *
103
+ * This is the counterpart to the implicit HOLD gate. When a workflow is
104
+ * paused by a HOLD directive, this gate defines the condition that will
105
+ * release the hold — receiving a RESUME directive.
106
+ *
107
+ * The created gate matches:
108
+ * - Directive source: first line of comment
109
+ * - Pattern: `^resume$` (exact match for RESUME directive)
110
+ *
111
+ * @returns A GateDefinition representing the implicit RESUME gate
112
+ */
113
+ export declare function createImplicitResumeGate(): GateDefinition;
114
+ //# sourceMappingURL=signal-gate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signal-gate.d.ts","sourceRoot":"","sources":["../../../../src/workflow/gates/signal-gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AAa9E;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,sFAAsF;IACtF,MAAM,EAAE,SAAS,GAAG,WAAW,CAAA;IAC/B,kEAAkE;IAClE,KAAK,EAAE,MAAM,CAAA;CACd;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,0DAA0D;IAC1D,OAAO,EAAE,OAAO,CAAA;IAChB,oEAAoE;IACpE,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAMD;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,iBAAiB,CAM5H;AAwBD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,gBAAgB,CAgD1G;AAMD;;;;;;;;;;;GAWG;AACH,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,kBAAkB,EAAE,KAAK,EAAE,MAAM,GAAG,cAAc,EAAE,CAiBtG;AAMD;;GAEG;AACH,eAAO,MAAM,uBAAuB,oBAAoB,CAAA;AAExD;;GAEG;AACH,eAAO,MAAM,yBAAyB,sBAAsB,CAAA;AAE5D;;;;;;;;;;;;;GAaG;AACH,wBAAgB,sBAAsB,IAAI,cAAc,CAUvD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,wBAAwB,IAAI,cAAc,CAUzD"}
@@ -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":""}