@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 @@
1
+ {"version":3,"file":"concurrency-semaphore.d.ts","sourceRoot":"","sources":["../../../src/workflow/concurrency-semaphore.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,qBAAa,oBAAoB;IAInB,OAAO,CAAC,QAAQ,CAAC,aAAa;IAH1C,OAAO,CAAC,OAAO,CAAI;IACnB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwB;gBAEnB,aAAa,EAAE,MAAM;IAMlD,qCAAqC;IACrC,IAAI,WAAW,IAAI,MAAM,CAExB;IAED,qCAAqC;IACrC,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,yDAAyD;IACnD,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAa9B,uDAAuD;IACvD,OAAO,IAAI,IAAI;CAOhB"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Concurrency Semaphore
3
+ *
4
+ * Limits the number of concurrent operations. When the limit is reached,
5
+ * subsequent acquire() calls wait until a slot is released.
6
+ */
7
+ export class ConcurrencySemaphore {
8
+ maxConcurrent;
9
+ current = 0;
10
+ waiters = [];
11
+ constructor(maxConcurrent) {
12
+ this.maxConcurrent = maxConcurrent;
13
+ if (maxConcurrent < 1) {
14
+ throw new Error('maxConcurrent must be at least 1');
15
+ }
16
+ }
17
+ /** Current number of active slots */
18
+ get activeCount() {
19
+ return this.current;
20
+ }
21
+ /** Number of waiters in the queue */
22
+ get waitingCount() {
23
+ return this.waiters.length;
24
+ }
25
+ /** Acquire a slot. Resolves when a slot is available. */
26
+ async acquire() {
27
+ if (this.current < this.maxConcurrent) {
28
+ this.current++;
29
+ return;
30
+ }
31
+ return new Promise((resolve) => {
32
+ this.waiters.push(() => {
33
+ this.current++;
34
+ resolve();
35
+ });
36
+ });
37
+ }
38
+ /** Release a slot. Wakes up the next waiter if any. */
39
+ release() {
40
+ this.current--;
41
+ if (this.waiters.length > 0) {
42
+ const next = this.waiters.shift();
43
+ next();
44
+ }
45
+ }
46
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=concurrency-semaphore.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"concurrency-semaphore.test.d.ts","sourceRoot":"","sources":["../../../src/workflow/concurrency-semaphore.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,183 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { ConcurrencySemaphore } from './concurrency-semaphore.js';
3
+ describe('ConcurrencySemaphore', () => {
4
+ describe('construction', () => {
5
+ it('creates with valid maxConcurrent', () => {
6
+ const sem = new ConcurrencySemaphore(3);
7
+ expect(sem.activeCount).toBe(0);
8
+ expect(sem.waitingCount).toBe(0);
9
+ });
10
+ it('creates with maxConcurrent = 1', () => {
11
+ const sem = new ConcurrencySemaphore(1);
12
+ expect(sem.activeCount).toBe(0);
13
+ });
14
+ it('throws when maxConcurrent is 0', () => {
15
+ expect(() => new ConcurrencySemaphore(0)).toThrow('maxConcurrent must be at least 1');
16
+ });
17
+ it('throws when maxConcurrent is negative', () => {
18
+ expect(() => new ConcurrencySemaphore(-1)).toThrow('maxConcurrent must be at least 1');
19
+ });
20
+ });
21
+ describe('acquire/release basic flow', () => {
22
+ it('acquires immediately when under capacity', async () => {
23
+ const sem = new ConcurrencySemaphore(2);
24
+ await sem.acquire();
25
+ expect(sem.activeCount).toBe(1);
26
+ await sem.acquire();
27
+ expect(sem.activeCount).toBe(2);
28
+ });
29
+ it('releases correctly and decrements activeCount', async () => {
30
+ const sem = new ConcurrencySemaphore(2);
31
+ await sem.acquire();
32
+ await sem.acquire();
33
+ expect(sem.activeCount).toBe(2);
34
+ sem.release();
35
+ expect(sem.activeCount).toBe(1);
36
+ sem.release();
37
+ expect(sem.activeCount).toBe(0);
38
+ });
39
+ });
40
+ describe('blocking behavior', () => {
41
+ it('blocks acquire when at capacity', async () => {
42
+ const sem = new ConcurrencySemaphore(1);
43
+ await sem.acquire();
44
+ expect(sem.activeCount).toBe(1);
45
+ let acquired = false;
46
+ const pending = sem.acquire().then(() => {
47
+ acquired = true;
48
+ });
49
+ // The waiter should be queued but not yet resolved
50
+ await Promise.resolve(); // flush microtasks
51
+ expect(acquired).toBe(false);
52
+ expect(sem.waitingCount).toBe(1);
53
+ // Release to unblock
54
+ sem.release();
55
+ await pending;
56
+ expect(acquired).toBe(true);
57
+ expect(sem.activeCount).toBe(1);
58
+ expect(sem.waitingCount).toBe(0);
59
+ });
60
+ });
61
+ describe('FIFO waiter ordering', () => {
62
+ it('releases waiters in FIFO order', async () => {
63
+ const sem = new ConcurrencySemaphore(1);
64
+ await sem.acquire();
65
+ const order = [];
66
+ const p1 = sem.acquire().then(() => {
67
+ order.push(1);
68
+ });
69
+ const p2 = sem.acquire().then(() => {
70
+ order.push(2);
71
+ });
72
+ const p3 = sem.acquire().then(() => {
73
+ order.push(3);
74
+ });
75
+ expect(sem.waitingCount).toBe(3);
76
+ // Release one at a time and let the microtask queue flush
77
+ sem.release();
78
+ await p1;
79
+ sem.release();
80
+ await p2;
81
+ sem.release();
82
+ await p3;
83
+ expect(order).toEqual([1, 2, 3]);
84
+ });
85
+ });
86
+ describe('activeCount and waitingCount tracking', () => {
87
+ it('tracks counts accurately through lifecycle', async () => {
88
+ const sem = new ConcurrencySemaphore(2);
89
+ expect(sem.activeCount).toBe(0);
90
+ expect(sem.waitingCount).toBe(0);
91
+ await sem.acquire();
92
+ expect(sem.activeCount).toBe(1);
93
+ expect(sem.waitingCount).toBe(0);
94
+ await sem.acquire();
95
+ expect(sem.activeCount).toBe(2);
96
+ expect(sem.waitingCount).toBe(0);
97
+ // Third acquire should wait
98
+ const pending = sem.acquire();
99
+ // Flush microtasks to make sure the promise callback has run
100
+ await Promise.resolve();
101
+ expect(sem.activeCount).toBe(2);
102
+ expect(sem.waitingCount).toBe(1);
103
+ sem.release();
104
+ await pending;
105
+ expect(sem.activeCount).toBe(2);
106
+ expect(sem.waitingCount).toBe(0);
107
+ sem.release();
108
+ expect(sem.activeCount).toBe(1);
109
+ sem.release();
110
+ expect(sem.activeCount).toBe(0);
111
+ });
112
+ });
113
+ describe('maxConcurrent=1 enforces serial execution', () => {
114
+ it('only one task runs at a time', async () => {
115
+ const sem = new ConcurrencySemaphore(1);
116
+ const log = [];
117
+ const runTask = async (name) => {
118
+ await sem.acquire();
119
+ log.push(`${name}:start`);
120
+ // Simulate async work
121
+ await new Promise((resolve) => setTimeout(resolve, 10));
122
+ log.push(`${name}:end`);
123
+ sem.release();
124
+ };
125
+ await Promise.all([runTask('a'), runTask('b'), runTask('c')]);
126
+ // Each task must start after the previous one ends
127
+ // The pattern should be: start, end, start, end, start, end
128
+ for (let i = 0; i < log.length - 1; i += 2) {
129
+ expect(log[i]).toMatch(/:start$/);
130
+ expect(log[i + 1]).toMatch(/:end$/);
131
+ }
132
+ // Verify no two starts happen before an end
133
+ let active = 0;
134
+ for (const entry of log) {
135
+ if (entry.endsWith(':start'))
136
+ active++;
137
+ if (entry.endsWith(':end'))
138
+ active--;
139
+ expect(active).toBeLessThanOrEqual(1);
140
+ }
141
+ });
142
+ });
143
+ describe('maxConcurrent=2 with 5 tasks verifies queuing behavior', () => {
144
+ it('runs at most 2 tasks concurrently', async () => {
145
+ const sem = new ConcurrencySemaphore(2);
146
+ let peakConcurrent = 0;
147
+ let currentConcurrent = 0;
148
+ const taskOrder = [];
149
+ const runTask = async (id) => {
150
+ await sem.acquire();
151
+ currentConcurrent++;
152
+ peakConcurrent = Math.max(peakConcurrent, currentConcurrent);
153
+ taskOrder.push(`start:${id}`);
154
+ // Simulate varying amounts of work
155
+ await new Promise((resolve) => setTimeout(resolve, 5 + id * 2));
156
+ taskOrder.push(`end:${id}`);
157
+ currentConcurrent--;
158
+ sem.release();
159
+ };
160
+ await Promise.all([
161
+ runTask(1),
162
+ runTask(2),
163
+ runTask(3),
164
+ runTask(4),
165
+ runTask(5),
166
+ ]);
167
+ // Peak concurrency should never exceed 2
168
+ expect(peakConcurrent).toBe(2);
169
+ // All 5 tasks should have started and ended
170
+ expect(taskOrder.filter((e) => e.startsWith('start:'))).toHaveLength(5);
171
+ expect(taskOrder.filter((e) => e.startsWith('end:'))).toHaveLength(5);
172
+ // Verify at no point more than 2 tasks are active
173
+ let active = 0;
174
+ for (const entry of taskOrder) {
175
+ if (entry.startsWith('start:'))
176
+ active++;
177
+ if (entry.startsWith('end:'))
178
+ active--;
179
+ expect(active).toBeLessThanOrEqual(2);
180
+ }
181
+ });
182
+ });
183
+ });
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Gate State Persistence Layer
3
+ *
4
+ * Manages gate lifecycle state for workflow execution gates (signal, timer, webhook).
5
+ * Uses a storage adapter pattern so that packages/core does not depend on
6
+ * packages/server (Redis) directly.
7
+ *
8
+ * Gates are external conditions that can pause workflow phase transitions until
9
+ * a condition is met (e.g., human approval signal, timer expiration, webhook callback).
10
+ */
11
+ import type { GateDefinition } from './workflow-types.js';
12
+ /**
13
+ * Persisted state for a single gate instance tied to an issue
14
+ */
15
+ export interface GateState {
16
+ /** The issue this gate is associated with */
17
+ issueId: string;
18
+ /** Unique gate name (from GateDefinition) */
19
+ gateName: string;
20
+ /** Gate type: signal (external event), timer (time-based), webhook (HTTP callback) */
21
+ gateType: 'signal' | 'timer' | 'webhook';
22
+ /** Current gate status */
23
+ status: 'pending' | 'active' | 'satisfied' | 'timed-out';
24
+ /** When the gate was triggered (phase entered), epoch ms */
25
+ activatedAt: number;
26
+ /** When the gate condition was met, epoch ms */
27
+ satisfiedAt?: number;
28
+ /** When the timeout fired, epoch ms */
29
+ timedOutAt?: number;
30
+ /** Action to take when gate times out */
31
+ timeoutAction?: 'escalate' | 'skip' | 'fail';
32
+ /** What satisfied the gate (comment ID, webhook payload hash, timer ID) */
33
+ signalSource?: string;
34
+ /** Authentication token for webhook gates (used to validate incoming callbacks) */
35
+ webhookToken?: string;
36
+ /** Duration string from gate definition (e.g., "4h") */
37
+ timeoutDuration?: string;
38
+ /** Computed absolute timestamp for timeout deadline, epoch ms */
39
+ timeoutDeadline?: number;
40
+ }
41
+ /**
42
+ * Storage adapter for gate state persistence.
43
+ * Implementations can back this with Redis, in-memory maps, etc.
44
+ */
45
+ export interface GateStorage {
46
+ /** Get the state of a specific gate for an issue */
47
+ getGateState(issueId: string, gateName: string): Promise<GateState | null>;
48
+ /** Set the state of a specific gate for an issue */
49
+ setGateState(issueId: string, gateName: string, state: GateState): Promise<void>;
50
+ /** Get all active (status === 'active') gates for an issue */
51
+ getActiveGates(issueId: string): Promise<GateState[]>;
52
+ /** Clear all gate states for an issue */
53
+ clearGateStates(issueId: string): Promise<void>;
54
+ }
55
+ /**
56
+ * In-memory gate storage for testing and local development
57
+ */
58
+ export declare class InMemoryGateStorage implements GateStorage {
59
+ private store;
60
+ /** Build a composite key from issueId and gateName */
61
+ private key;
62
+ getGateState(issueId: string, gateName: string): Promise<GateState | null>;
63
+ setGateState(issueId: string, gateName: string, state: GateState): Promise<void>;
64
+ getActiveGates(issueId: string): Promise<GateState[]>;
65
+ clearGateStates(issueId: string): Promise<void>;
66
+ }
67
+ /**
68
+ * Initialize the gate state manager with a storage adapter.
69
+ * Must be called before using gate state management functions.
70
+ */
71
+ export declare function initGateStorage(storage: GateStorage): void;
72
+ /**
73
+ * Parse a duration string into milliseconds.
74
+ *
75
+ * Supported formats:
76
+ * - "15s" - seconds
77
+ * - "30m" - minutes
78
+ * - "4h" - hours
79
+ * - "2d" - days
80
+ *
81
+ * @param duration - Duration string (e.g., "4h", "30m", "2d", "15s")
82
+ * @returns Duration in milliseconds
83
+ * @throws Error if the duration format is invalid
84
+ */
85
+ export declare function parseDuration(duration: string): number;
86
+ /**
87
+ * Activate a gate for an issue, creating an active gate state with a computed
88
+ * timeout deadline (if the gate definition includes a timeout).
89
+ *
90
+ * @param issueId - The issue identifier
91
+ * @param gateDef - The gate definition from the workflow
92
+ * @param storage - The gate storage adapter to use
93
+ * @returns The created GateState
94
+ */
95
+ export declare function activateGate(issueId: string, gateDef: GateDefinition, storage: GateStorage): Promise<GateState>;
96
+ /**
97
+ * Mark a gate as satisfied, recording what source satisfied it.
98
+ *
99
+ * @param issueId - The issue identifier
100
+ * @param gateName - The gate name to satisfy
101
+ * @param source - What satisfied the gate (e.g., comment ID, webhook payload hash, timer ID)
102
+ * @param storage - The gate storage adapter to use
103
+ * @returns The updated GateState, or null if the gate was not found
104
+ */
105
+ export declare function satisfyGate(issueId: string, gateName: string, source: string, storage: GateStorage): Promise<GateState | null>;
106
+ /**
107
+ * Mark a gate as timed-out.
108
+ *
109
+ * @param issueId - The issue identifier
110
+ * @param gateName - The gate name to time out
111
+ * @param storage - The gate storage adapter to use
112
+ * @returns The updated GateState, or null if the gate was not found
113
+ */
114
+ export declare function timeoutGate(issueId: string, gateName: string, storage: GateStorage): Promise<GateState | null>;
115
+ //# sourceMappingURL=gate-state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gate-state.d.ts","sourceRoot":"","sources":["../../../src/workflow/gate-state.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Gate State Persistence Layer
3
+ *
4
+ * Manages gate lifecycle state for workflow execution gates (signal, timer, webhook).
5
+ * Uses a storage adapter pattern so that packages/core does not depend on
6
+ * packages/server (Redis) directly.
7
+ *
8
+ * Gates are external conditions that can pause workflow phase transitions until
9
+ * a condition is met (e.g., human approval signal, timer expiration, webhook callback).
10
+ */
11
+ const log = {
12
+ info: (msg, data) => console.log(`[gate-state] ${msg}`, data ? JSON.stringify(data) : ''),
13
+ warn: (msg, data) => console.warn(`[gate-state] ${msg}`, data ? JSON.stringify(data) : ''),
14
+ error: (msg, data) => console.error(`[gate-state] ${msg}`, data ? JSON.stringify(data) : ''),
15
+ debug: (_msg, _data) => { },
16
+ };
17
+ /**
18
+ * In-memory gate storage for testing and local development
19
+ */
20
+ export class InMemoryGateStorage {
21
+ store = new Map();
22
+ /** Build a composite key from issueId and gateName */
23
+ key(issueId, gateName) {
24
+ return `${issueId}:${gateName}`;
25
+ }
26
+ async getGateState(issueId, gateName) {
27
+ return this.store.get(this.key(issueId, gateName)) ?? null;
28
+ }
29
+ async setGateState(issueId, gateName, state) {
30
+ this.store.set(this.key(issueId, gateName), state);
31
+ }
32
+ async getActiveGates(issueId) {
33
+ const results = [];
34
+ for (const [key, state] of this.store) {
35
+ if (key.startsWith(`${issueId}:`) && state.status === 'active') {
36
+ results.push(state);
37
+ }
38
+ }
39
+ return results;
40
+ }
41
+ async clearGateStates(issueId) {
42
+ const keysToDelete = [];
43
+ for (const key of this.store.keys()) {
44
+ if (key.startsWith(`${issueId}:`)) {
45
+ keysToDelete.push(key);
46
+ }
47
+ }
48
+ for (const key of keysToDelete) {
49
+ this.store.delete(key);
50
+ }
51
+ }
52
+ }
53
+ // ============================================
54
+ // Module-level storage reference
55
+ // ============================================
56
+ let _storage = null;
57
+ /**
58
+ * Initialize the gate state manager with a storage adapter.
59
+ * Must be called before using gate state management functions.
60
+ */
61
+ export function initGateStorage(storage) {
62
+ _storage = storage;
63
+ log.info('Gate storage initialized');
64
+ }
65
+ /**
66
+ * Get the current storage adapter, throwing if not initialized
67
+ */
68
+ function getStorage() {
69
+ if (!_storage) {
70
+ throw new Error('Gate storage not initialized. Call initGateStorage() first.');
71
+ }
72
+ return _storage;
73
+ }
74
+ // ============================================
75
+ // Duration Parsing
76
+ // ============================================
77
+ /**
78
+ * Parse a duration string into milliseconds.
79
+ *
80
+ * Supported formats:
81
+ * - "15s" - seconds
82
+ * - "30m" - minutes
83
+ * - "4h" - hours
84
+ * - "2d" - days
85
+ *
86
+ * @param duration - Duration string (e.g., "4h", "30m", "2d", "15s")
87
+ * @returns Duration in milliseconds
88
+ * @throws Error if the duration format is invalid
89
+ */
90
+ export function parseDuration(duration) {
91
+ const match = duration.match(/^(\d+)(s|m|h|d)$/);
92
+ if (!match) {
93
+ throw new Error(`Invalid duration format: "${duration}". Expected format like "4h", "30m", "2d", "15s".`);
94
+ }
95
+ const value = parseInt(match[1], 10);
96
+ const unit = match[2];
97
+ switch (unit) {
98
+ case 's': return value * 1000;
99
+ case 'm': return value * 60 * 1000;
100
+ case 'h': return value * 60 * 60 * 1000;
101
+ case 'd': return value * 24 * 60 * 60 * 1000;
102
+ default: throw new Error(`Unknown duration unit: "${unit}"`);
103
+ }
104
+ }
105
+ // ============================================
106
+ // Gate Lifecycle Helpers
107
+ // ============================================
108
+ /**
109
+ * Activate a gate for an issue, creating an active gate state with a computed
110
+ * timeout deadline (if the gate definition includes a timeout).
111
+ *
112
+ * @param issueId - The issue identifier
113
+ * @param gateDef - The gate definition from the workflow
114
+ * @param storage - The gate storage adapter to use
115
+ * @returns The created GateState
116
+ */
117
+ export async function activateGate(issueId, gateDef, storage) {
118
+ const now = Date.now();
119
+ const state = {
120
+ issueId,
121
+ gateName: gateDef.name,
122
+ gateType: gateDef.type,
123
+ status: 'active',
124
+ activatedAt: now,
125
+ };
126
+ // Compute timeout deadline if gate has a timeout configuration
127
+ if (gateDef.timeout) {
128
+ state.timeoutAction = gateDef.timeout.action;
129
+ state.timeoutDuration = gateDef.timeout.duration;
130
+ state.timeoutDeadline = now + parseDuration(gateDef.timeout.duration);
131
+ }
132
+ await storage.setGateState(issueId, gateDef.name, state);
133
+ log.info('Gate activated', { issueId, gateName: gateDef.name, gateType: gateDef.type });
134
+ return state;
135
+ }
136
+ /**
137
+ * Mark a gate as satisfied, recording what source satisfied it.
138
+ *
139
+ * @param issueId - The issue identifier
140
+ * @param gateName - The gate name to satisfy
141
+ * @param source - What satisfied the gate (e.g., comment ID, webhook payload hash, timer ID)
142
+ * @param storage - The gate storage adapter to use
143
+ * @returns The updated GateState, or null if the gate was not found
144
+ */
145
+ export async function satisfyGate(issueId, gateName, source, storage) {
146
+ const state = await storage.getGateState(issueId, gateName);
147
+ if (!state) {
148
+ log.warn('Cannot satisfy gate: not found', { issueId, gateName });
149
+ return null;
150
+ }
151
+ if (state.status !== 'active') {
152
+ log.warn('Cannot satisfy gate: not in active status', { issueId, gateName, status: state.status });
153
+ return null;
154
+ }
155
+ state.status = 'satisfied';
156
+ state.satisfiedAt = Date.now();
157
+ state.signalSource = source;
158
+ await storage.setGateState(issueId, gateName, state);
159
+ log.info('Gate satisfied', { issueId, gateName, source });
160
+ return state;
161
+ }
162
+ /**
163
+ * Mark a gate as timed-out.
164
+ *
165
+ * @param issueId - The issue identifier
166
+ * @param gateName - The gate name to time out
167
+ * @param storage - The gate storage adapter to use
168
+ * @returns The updated GateState, or null if the gate was not found
169
+ */
170
+ export async function timeoutGate(issueId, gateName, storage) {
171
+ const state = await storage.getGateState(issueId, gateName);
172
+ if (!state) {
173
+ log.warn('Cannot timeout gate: not found', { issueId, gateName });
174
+ return null;
175
+ }
176
+ if (state.status !== 'active') {
177
+ log.warn('Cannot timeout gate: not in active status', { issueId, gateName, status: state.status });
178
+ return null;
179
+ }
180
+ state.status = 'timed-out';
181
+ state.timedOutAt = Date.now();
182
+ await storage.setGateState(issueId, gateName, state);
183
+ log.info('Gate timed out', { issueId, gateName, timeoutAction: state.timeoutAction });
184
+ return state;
185
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=gate-state.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gate-state.test.d.ts","sourceRoot":"","sources":["../../../src/workflow/gate-state.test.ts"],"names":[],"mappings":""}