@renseiai/agentfactory-linear 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +91 -0
- package/dist/src/agent-client-project-repo.test.d.ts +2 -0
- package/dist/src/agent-client-project-repo.test.d.ts.map +1 -0
- package/dist/src/agent-client-project-repo.test.js +153 -0
- package/dist/src/agent-client.d.ts +261 -0
- package/dist/src/agent-client.d.ts.map +1 -0
- package/dist/src/agent-client.js +902 -0
- package/dist/src/agent-session.d.ts +303 -0
- package/dist/src/agent-session.d.ts.map +1 -0
- package/dist/src/agent-session.js +969 -0
- package/dist/src/checkbox-utils.d.ts +88 -0
- package/dist/src/checkbox-utils.d.ts.map +1 -0
- package/dist/src/checkbox-utils.js +120 -0
- package/dist/src/circuit-breaker.d.ts +76 -0
- package/dist/src/circuit-breaker.d.ts.map +1 -0
- package/dist/src/circuit-breaker.js +229 -0
- package/dist/src/circuit-breaker.test.d.ts +2 -0
- package/dist/src/circuit-breaker.test.d.ts.map +1 -0
- package/dist/src/circuit-breaker.test.js +292 -0
- package/dist/src/constants.d.ts +87 -0
- package/dist/src/constants.d.ts.map +1 -0
- package/dist/src/constants.js +101 -0
- package/dist/src/defaults/auto-trigger.d.ts +35 -0
- package/dist/src/defaults/auto-trigger.d.ts.map +1 -0
- package/dist/src/defaults/auto-trigger.js +36 -0
- package/dist/src/defaults/index.d.ts +12 -0
- package/dist/src/defaults/index.d.ts.map +1 -0
- package/dist/src/defaults/index.js +11 -0
- package/dist/src/defaults/priority.d.ts +20 -0
- package/dist/src/defaults/priority.d.ts.map +1 -0
- package/dist/src/defaults/priority.js +37 -0
- package/dist/src/defaults/prompts.d.ts +42 -0
- package/dist/src/defaults/prompts.d.ts.map +1 -0
- package/dist/src/defaults/prompts.js +310 -0
- package/dist/src/defaults/prompts.test.d.ts +2 -0
- package/dist/src/defaults/prompts.test.d.ts.map +1 -0
- package/dist/src/defaults/prompts.test.js +263 -0
- package/dist/src/defaults/work-type-detection.d.ts +19 -0
- package/dist/src/defaults/work-type-detection.d.ts.map +1 -0
- package/dist/src/defaults/work-type-detection.js +93 -0
- package/dist/src/errors.d.ts +91 -0
- package/dist/src/errors.d.ts.map +1 -0
- package/dist/src/errors.js +173 -0
- package/dist/src/frontend-adapter.d.ts +168 -0
- package/dist/src/frontend-adapter.d.ts.map +1 -0
- package/dist/src/frontend-adapter.js +314 -0
- package/dist/src/frontend-adapter.test.d.ts +2 -0
- package/dist/src/frontend-adapter.test.d.ts.map +1 -0
- package/dist/src/frontend-adapter.test.js +545 -0
- package/dist/src/index.d.ts +28 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +30 -0
- package/dist/src/issue-tracker-proxy.d.ts +140 -0
- package/dist/src/issue-tracker-proxy.d.ts.map +1 -0
- package/dist/src/issue-tracker-proxy.js +10 -0
- package/dist/src/platform-adapter.d.ts +132 -0
- package/dist/src/platform-adapter.d.ts.map +1 -0
- package/dist/src/platform-adapter.js +260 -0
- package/dist/src/platform-adapter.test.d.ts +2 -0
- package/dist/src/platform-adapter.test.d.ts.map +1 -0
- package/dist/src/platform-adapter.test.js +468 -0
- package/dist/src/proxy-client.d.ts +103 -0
- package/dist/src/proxy-client.d.ts.map +1 -0
- package/dist/src/proxy-client.js +191 -0
- package/dist/src/rate-limiter.d.ts +64 -0
- package/dist/src/rate-limiter.d.ts.map +1 -0
- package/dist/src/rate-limiter.js +163 -0
- package/dist/src/rate-limiter.test.d.ts +2 -0
- package/dist/src/rate-limiter.test.d.ts.map +1 -0
- package/dist/src/rate-limiter.test.js +217 -0
- package/dist/src/retry.d.ts +59 -0
- package/dist/src/retry.d.ts.map +1 -0
- package/dist/src/retry.js +82 -0
- package/dist/src/types.d.ts +492 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +143 -0
- package/dist/src/utils.d.ts +52 -0
- package/dist/src/utils.d.ts.map +1 -0
- package/dist/src/utils.js +277 -0
- package/dist/src/webhook-types.d.ts +308 -0
- package/dist/src/webhook-types.d.ts.map +1 -0
- package/dist/src/webhook-types.js +46 -0
- package/package.json +70 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { CircuitBreaker, DEFAULT_CIRCUIT_BREAKER_CONFIG } from './circuit-breaker.js';
|
|
3
|
+
import { CircuitOpenError } from './errors.js';
|
|
4
|
+
describe('CircuitBreaker', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
vi.useFakeTimers();
|
|
7
|
+
});
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
vi.useRealTimers();
|
|
10
|
+
});
|
|
11
|
+
// ========================================================================
|
|
12
|
+
// Construction & defaults
|
|
13
|
+
// ========================================================================
|
|
14
|
+
it('starts in closed state', () => {
|
|
15
|
+
const cb = new CircuitBreaker();
|
|
16
|
+
expect(cb.state).toBe('closed');
|
|
17
|
+
});
|
|
18
|
+
it('uses default config when none provided', () => {
|
|
19
|
+
const cb = new CircuitBreaker();
|
|
20
|
+
const status = cb.getStatus();
|
|
21
|
+
expect(status.state).toBe('closed');
|
|
22
|
+
expect(status.consecutiveFailures).toBe(0);
|
|
23
|
+
expect(status.currentResetTimeoutMs).toBe(DEFAULT_CIRCUIT_BREAKER_CONFIG.resetTimeoutMs);
|
|
24
|
+
});
|
|
25
|
+
it('accepts custom config', () => {
|
|
26
|
+
const cb = new CircuitBreaker({ failureThreshold: 5, resetTimeoutMs: 30_000 });
|
|
27
|
+
// Trip it 4 times — should still be closed
|
|
28
|
+
for (let i = 0; i < 4; i++) {
|
|
29
|
+
cb.recordAuthFailure(401);
|
|
30
|
+
}
|
|
31
|
+
expect(cb.state).toBe('closed');
|
|
32
|
+
// 5th failure trips it
|
|
33
|
+
cb.recordAuthFailure(401);
|
|
34
|
+
expect(cb.state).toBe('open');
|
|
35
|
+
});
|
|
36
|
+
// ========================================================================
|
|
37
|
+
// canProceed
|
|
38
|
+
// ========================================================================
|
|
39
|
+
it('allows calls when closed', () => {
|
|
40
|
+
const cb = new CircuitBreaker();
|
|
41
|
+
expect(cb.canProceed()).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
it('blocks calls when open', () => {
|
|
44
|
+
const cb = new CircuitBreaker({ failureThreshold: 1 });
|
|
45
|
+
cb.recordAuthFailure(401);
|
|
46
|
+
expect(cb.state).toBe('open');
|
|
47
|
+
expect(cb.canProceed()).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
it('allows one probe call when half-open', () => {
|
|
50
|
+
const cb = new CircuitBreaker({ failureThreshold: 1, resetTimeoutMs: 1000 });
|
|
51
|
+
cb.recordAuthFailure(401);
|
|
52
|
+
expect(cb.state).toBe('open');
|
|
53
|
+
// Advance past reset timeout
|
|
54
|
+
vi.advanceTimersByTime(1001);
|
|
55
|
+
expect(cb.state).toBe('half-open');
|
|
56
|
+
// First call should be allowed (probe)
|
|
57
|
+
expect(cb.canProceed()).toBe(true);
|
|
58
|
+
// Second call should be blocked (probe already in-flight)
|
|
59
|
+
expect(cb.canProceed()).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
// ========================================================================
|
|
62
|
+
// State transitions: closed → open
|
|
63
|
+
// ========================================================================
|
|
64
|
+
it('trips after consecutive auth failures reach threshold', () => {
|
|
65
|
+
const cb = new CircuitBreaker({ failureThreshold: 2 });
|
|
66
|
+
cb.recordAuthFailure(400);
|
|
67
|
+
expect(cb.state).toBe('closed');
|
|
68
|
+
cb.recordAuthFailure(401);
|
|
69
|
+
expect(cb.state).toBe('open');
|
|
70
|
+
});
|
|
71
|
+
it('always counts failures regardless of status code (isAuthError already vetted)', () => {
|
|
72
|
+
const cb = new CircuitBreaker({ failureThreshold: 2 });
|
|
73
|
+
cb.recordAuthFailure(200); // e.g., GraphQL 200 with auth error in body
|
|
74
|
+
expect(cb.getStatus().consecutiveFailures).toBe(1);
|
|
75
|
+
cb.recordAuthFailure(); // no status code at all
|
|
76
|
+
expect(cb.state).toBe('open');
|
|
77
|
+
});
|
|
78
|
+
it('resets failure count on success', () => {
|
|
79
|
+
const cb = new CircuitBreaker({ failureThreshold: 3 });
|
|
80
|
+
cb.recordAuthFailure(401); // 1
|
|
81
|
+
cb.recordAuthFailure(403); // 2
|
|
82
|
+
cb.recordSuccess(); // reset to 0
|
|
83
|
+
cb.recordAuthFailure(400); // 1 — should NOT trip
|
|
84
|
+
expect(cb.state).toBe('closed');
|
|
85
|
+
});
|
|
86
|
+
// ========================================================================
|
|
87
|
+
// State transitions: open → half-open
|
|
88
|
+
// ========================================================================
|
|
89
|
+
it('transitions from open to half-open after resetTimeoutMs', () => {
|
|
90
|
+
const cb = new CircuitBreaker({ failureThreshold: 1, resetTimeoutMs: 5000 });
|
|
91
|
+
cb.recordAuthFailure(401);
|
|
92
|
+
expect(cb.state).toBe('open');
|
|
93
|
+
// Not enough time
|
|
94
|
+
vi.advanceTimersByTime(4999);
|
|
95
|
+
expect(cb.state).toBe('open');
|
|
96
|
+
// Exactly enough time
|
|
97
|
+
vi.advanceTimersByTime(1);
|
|
98
|
+
expect(cb.state).toBe('half-open');
|
|
99
|
+
});
|
|
100
|
+
// ========================================================================
|
|
101
|
+
// State transitions: half-open → closed (probe success)
|
|
102
|
+
// ========================================================================
|
|
103
|
+
it('closes circuit on successful probe in half-open', () => {
|
|
104
|
+
const cb = new CircuitBreaker({ failureThreshold: 1, resetTimeoutMs: 1000 });
|
|
105
|
+
cb.recordAuthFailure(401);
|
|
106
|
+
vi.advanceTimersByTime(1001);
|
|
107
|
+
expect(cb.state).toBe('half-open');
|
|
108
|
+
// Probe succeeds
|
|
109
|
+
cb.canProceed(); // acquire probe
|
|
110
|
+
cb.recordSuccess();
|
|
111
|
+
expect(cb.state).toBe('closed');
|
|
112
|
+
expect(cb.getStatus().consecutiveFailures).toBe(0);
|
|
113
|
+
});
|
|
114
|
+
// ========================================================================
|
|
115
|
+
// State transitions: half-open → open (probe failure + exponential backoff)
|
|
116
|
+
// ========================================================================
|
|
117
|
+
it('reopens circuit on failed probe with exponential backoff', () => {
|
|
118
|
+
const cb = new CircuitBreaker({
|
|
119
|
+
failureThreshold: 1,
|
|
120
|
+
resetTimeoutMs: 1000,
|
|
121
|
+
maxResetTimeoutMs: 16000,
|
|
122
|
+
backoffMultiplier: 2,
|
|
123
|
+
});
|
|
124
|
+
// First trip
|
|
125
|
+
cb.recordAuthFailure(401);
|
|
126
|
+
expect(cb.state).toBe('open');
|
|
127
|
+
expect(cb.getStatus().currentResetTimeoutMs).toBe(1000);
|
|
128
|
+
// Wait for half-open
|
|
129
|
+
vi.advanceTimersByTime(1001);
|
|
130
|
+
expect(cb.state).toBe('half-open');
|
|
131
|
+
// Probe fails → reopen with backoff
|
|
132
|
+
cb.canProceed();
|
|
133
|
+
cb.recordAuthFailure(401);
|
|
134
|
+
expect(cb.state).toBe('open');
|
|
135
|
+
expect(cb.getStatus().currentResetTimeoutMs).toBe(2000); // 1000 * 2
|
|
136
|
+
// Wait 2000ms for next half-open
|
|
137
|
+
vi.advanceTimersByTime(2001);
|
|
138
|
+
expect(cb.state).toBe('half-open');
|
|
139
|
+
// Probe fails again → further backoff
|
|
140
|
+
cb.canProceed();
|
|
141
|
+
cb.recordAuthFailure(401);
|
|
142
|
+
expect(cb.state).toBe('open');
|
|
143
|
+
expect(cb.getStatus().currentResetTimeoutMs).toBe(4000); // 2000 * 2
|
|
144
|
+
});
|
|
145
|
+
it('caps reset timeout at maxResetTimeoutMs', () => {
|
|
146
|
+
const cb = new CircuitBreaker({
|
|
147
|
+
failureThreshold: 1,
|
|
148
|
+
resetTimeoutMs: 1000,
|
|
149
|
+
maxResetTimeoutMs: 3000,
|
|
150
|
+
backoffMultiplier: 2,
|
|
151
|
+
});
|
|
152
|
+
// Trip → open (1000ms)
|
|
153
|
+
cb.recordAuthFailure(401);
|
|
154
|
+
vi.advanceTimersByTime(1001);
|
|
155
|
+
// Probe fail → 2000ms
|
|
156
|
+
cb.canProceed();
|
|
157
|
+
cb.recordAuthFailure(401);
|
|
158
|
+
vi.advanceTimersByTime(2001);
|
|
159
|
+
// Probe fail → 3000ms (capped at max)
|
|
160
|
+
cb.canProceed();
|
|
161
|
+
cb.recordAuthFailure(401);
|
|
162
|
+
expect(cb.getStatus().currentResetTimeoutMs).toBe(3000);
|
|
163
|
+
vi.advanceTimersByTime(3001);
|
|
164
|
+
// Probe fail → still 3000ms (capped)
|
|
165
|
+
cb.canProceed();
|
|
166
|
+
cb.recordAuthFailure(401);
|
|
167
|
+
expect(cb.getStatus().currentResetTimeoutMs).toBe(3000);
|
|
168
|
+
});
|
|
169
|
+
it('resets backoff on successful recovery', () => {
|
|
170
|
+
const cb = new CircuitBreaker({
|
|
171
|
+
failureThreshold: 1,
|
|
172
|
+
resetTimeoutMs: 1000,
|
|
173
|
+
maxResetTimeoutMs: 16000,
|
|
174
|
+
backoffMultiplier: 2,
|
|
175
|
+
});
|
|
176
|
+
// Trip and backoff to 2000ms
|
|
177
|
+
cb.recordAuthFailure(401);
|
|
178
|
+
vi.advanceTimersByTime(1001);
|
|
179
|
+
cb.canProceed();
|
|
180
|
+
cb.recordAuthFailure(401);
|
|
181
|
+
expect(cb.getStatus().currentResetTimeoutMs).toBe(2000);
|
|
182
|
+
// Recover
|
|
183
|
+
vi.advanceTimersByTime(2001);
|
|
184
|
+
cb.canProceed();
|
|
185
|
+
cb.recordSuccess();
|
|
186
|
+
expect(cb.state).toBe('closed');
|
|
187
|
+
expect(cb.getStatus().currentResetTimeoutMs).toBe(1000); // reset to original
|
|
188
|
+
});
|
|
189
|
+
// ========================================================================
|
|
190
|
+
// reset()
|
|
191
|
+
// ========================================================================
|
|
192
|
+
it('reset() returns to closed from any state', () => {
|
|
193
|
+
const cb = new CircuitBreaker({ failureThreshold: 1 });
|
|
194
|
+
cb.recordAuthFailure(401);
|
|
195
|
+
expect(cb.state).toBe('open');
|
|
196
|
+
cb.reset();
|
|
197
|
+
expect(cb.state).toBe('closed');
|
|
198
|
+
expect(cb.getStatus().consecutiveFailures).toBe(0);
|
|
199
|
+
expect(cb.getStatus().currentResetTimeoutMs).toBe(DEFAULT_CIRCUIT_BREAKER_CONFIG.resetTimeoutMs);
|
|
200
|
+
});
|
|
201
|
+
// ========================================================================
|
|
202
|
+
// isAuthError
|
|
203
|
+
// ========================================================================
|
|
204
|
+
it('detects auth errors by HTTP status code', () => {
|
|
205
|
+
const cb = new CircuitBreaker();
|
|
206
|
+
expect(cb.isAuthError({ status: 400 })).toBe(true);
|
|
207
|
+
expect(cb.isAuthError({ status: 401 })).toBe(true);
|
|
208
|
+
expect(cb.isAuthError({ status: 403 })).toBe(true);
|
|
209
|
+
expect(cb.isAuthError({ statusCode: 401 })).toBe(true);
|
|
210
|
+
expect(cb.isAuthError({ response: { status: 403 } })).toBe(true);
|
|
211
|
+
});
|
|
212
|
+
it('does not flag non-auth status codes', () => {
|
|
213
|
+
const cb = new CircuitBreaker();
|
|
214
|
+
expect(cb.isAuthError({ status: 200 })).toBe(false);
|
|
215
|
+
expect(cb.isAuthError({ status: 404 })).toBe(false);
|
|
216
|
+
expect(cb.isAuthError({ status: 500 })).toBe(false);
|
|
217
|
+
expect(cb.isAuthError({ status: 429 })).toBe(false); // rate limit is handled separately
|
|
218
|
+
});
|
|
219
|
+
it('detects GraphQL RATELIMITED error code', () => {
|
|
220
|
+
const cb = new CircuitBreaker();
|
|
221
|
+
// Direct extensions.code
|
|
222
|
+
expect(cb.isAuthError({ extensions: { code: 'RATELIMITED' } })).toBe(true);
|
|
223
|
+
// Nested errors array
|
|
224
|
+
expect(cb.isAuthError({
|
|
225
|
+
errors: [{ extensions: { code: 'RATELIMITED' } }],
|
|
226
|
+
})).toBe(true);
|
|
227
|
+
// In response body
|
|
228
|
+
expect(cb.isAuthError({
|
|
229
|
+
response: {
|
|
230
|
+
body: {
|
|
231
|
+
errors: [{ extensions: { code: 'RATELIMITED' } }],
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
})).toBe(true);
|
|
235
|
+
// In response data (alternative shape)
|
|
236
|
+
expect(cb.isAuthError({
|
|
237
|
+
response: {
|
|
238
|
+
data: {
|
|
239
|
+
errors: [{ extensions: { code: 'RATELIMITED' } }],
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
})).toBe(true);
|
|
243
|
+
});
|
|
244
|
+
it('detects auth errors by message pattern', () => {
|
|
245
|
+
const cb = new CircuitBreaker();
|
|
246
|
+
expect(cb.isAuthError({ message: 'Access denied - Only app users can create agent activities' })).toBe(true);
|
|
247
|
+
expect(cb.isAuthError({ message: 'Unauthorized request' })).toBe(true);
|
|
248
|
+
expect(cb.isAuthError({ message: 'Forbidden: insufficient permissions' })).toBe(true);
|
|
249
|
+
});
|
|
250
|
+
it('rejects non-error inputs', () => {
|
|
251
|
+
const cb = new CircuitBreaker();
|
|
252
|
+
expect(cb.isAuthError(null)).toBe(false);
|
|
253
|
+
expect(cb.isAuthError(undefined)).toBe(false);
|
|
254
|
+
expect(cb.isAuthError('string')).toBe(false);
|
|
255
|
+
expect(cb.isAuthError(42)).toBe(false);
|
|
256
|
+
});
|
|
257
|
+
it('detects RATELIMITED in error message', () => {
|
|
258
|
+
const cb = new CircuitBreaker();
|
|
259
|
+
expect(cb.isAuthError({ message: 'GraphQL Error: RATELIMITED' })).toBe(true);
|
|
260
|
+
});
|
|
261
|
+
// ========================================================================
|
|
262
|
+
// createOpenError
|
|
263
|
+
// ========================================================================
|
|
264
|
+
it('creates a CircuitOpenError with remaining time info', () => {
|
|
265
|
+
const cb = new CircuitBreaker({ failureThreshold: 1, resetTimeoutMs: 10_000 });
|
|
266
|
+
cb.recordAuthFailure(401);
|
|
267
|
+
// 3 seconds have passed
|
|
268
|
+
vi.advanceTimersByTime(3000);
|
|
269
|
+
const error = cb.createOpenError();
|
|
270
|
+
expect(error).toBeInstanceOf(CircuitOpenError);
|
|
271
|
+
expect(error.code).toBe('CIRCUIT_OPEN');
|
|
272
|
+
expect(error.retryAfterMs).toBeGreaterThan(0);
|
|
273
|
+
expect(error.retryAfterMs).toBeLessThanOrEqual(7000);
|
|
274
|
+
expect(error.message).toMatch(/Circuit breaker is open/);
|
|
275
|
+
});
|
|
276
|
+
// ========================================================================
|
|
277
|
+
// getStatus diagnostic info
|
|
278
|
+
// ========================================================================
|
|
279
|
+
it('provides diagnostic status info', () => {
|
|
280
|
+
const cb = new CircuitBreaker({ failureThreshold: 1, resetTimeoutMs: 5000 });
|
|
281
|
+
let status = cb.getStatus();
|
|
282
|
+
expect(status.state).toBe('closed');
|
|
283
|
+
expect(status.consecutiveFailures).toBe(0);
|
|
284
|
+
expect(status.msSinceOpened).toBeNull();
|
|
285
|
+
cb.recordAuthFailure(401);
|
|
286
|
+
vi.advanceTimersByTime(2000);
|
|
287
|
+
status = cb.getStatus();
|
|
288
|
+
expect(status.state).toBe('open');
|
|
289
|
+
expect(status.consecutiveFailures).toBe(1);
|
|
290
|
+
expect(status.msSinceOpened).toBeGreaterThanOrEqual(2000);
|
|
291
|
+
});
|
|
292
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linear API Constants
|
|
3
|
+
*
|
|
4
|
+
* Contains API limits, well-known IDs, and configuration values.
|
|
5
|
+
* All workspace-specific IDs must be provided via environment variables.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Maximum length for comment body.
|
|
9
|
+
* Linear doesn't publicly document this limit, but testing shows ~10k is safe.
|
|
10
|
+
* We use a conservative limit to avoid truncation issues.
|
|
11
|
+
*/
|
|
12
|
+
export declare const LINEAR_COMMENT_MAX_LENGTH = 10000;
|
|
13
|
+
/**
|
|
14
|
+
* Marker appended when content is truncated
|
|
15
|
+
*/
|
|
16
|
+
export declare const TRUNCATION_MARKER = "\n\n... (content truncated)";
|
|
17
|
+
/**
|
|
18
|
+
* Maximum number of comments for a single completion (safety limit)
|
|
19
|
+
*/
|
|
20
|
+
export declare const MAX_COMPLETION_COMMENTS = 10;
|
|
21
|
+
/**
|
|
22
|
+
* Characters reserved for part markers and overhead
|
|
23
|
+
*/
|
|
24
|
+
export declare const COMMENT_OVERHEAD = 100;
|
|
25
|
+
/**
|
|
26
|
+
* Continuation marker for multi-part comments
|
|
27
|
+
*/
|
|
28
|
+
export declare const CONTINUATION_MARKER = "\n\n*...continued in next comment*";
|
|
29
|
+
/**
|
|
30
|
+
* Default team UUID
|
|
31
|
+
* Must be set via LINEAR_TEAM_ID env var
|
|
32
|
+
*
|
|
33
|
+
* Uses a getter to read lazily from process.env, avoiding ESM import
|
|
34
|
+
* hoisting issues where the value is captured before dotenv runs.
|
|
35
|
+
*/
|
|
36
|
+
export declare function getDefaultTeamId(): string;
|
|
37
|
+
/**
|
|
38
|
+
* Default team name
|
|
39
|
+
* Can be set via LINEAR_TEAM_NAME env var (auto-set by orchestrator from issue context)
|
|
40
|
+
*/
|
|
41
|
+
export declare function getDefaultTeamName(): string;
|
|
42
|
+
/**
|
|
43
|
+
* Project IDs — must be set via env vars:
|
|
44
|
+
* - LINEAR_PROJECT_AGENT
|
|
45
|
+
* - LINEAR_PROJECT_SOCIAL
|
|
46
|
+
* - LINEAR_PROJECT_TEST
|
|
47
|
+
*/
|
|
48
|
+
export declare const LINEAR_PROJECTS: {
|
|
49
|
+
readonly AGENT: string;
|
|
50
|
+
readonly SOCIAL: string;
|
|
51
|
+
/** Test project for E2E testing of orchestrator */
|
|
52
|
+
readonly TEST: string;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Label IDs for issue classification
|
|
56
|
+
* Must be set via env vars:
|
|
57
|
+
* - LINEAR_LABEL_BUG
|
|
58
|
+
* - LINEAR_LABEL_FEATURE
|
|
59
|
+
* - LINEAR_LABEL_CHORE
|
|
60
|
+
*/
|
|
61
|
+
export declare const LINEAR_LABELS: {
|
|
62
|
+
readonly BUG: string;
|
|
63
|
+
readonly FEATURE: string;
|
|
64
|
+
readonly CHORE: string;
|
|
65
|
+
readonly NEEDS_HUMAN: string;
|
|
66
|
+
};
|
|
67
|
+
export declare const TEST_LABEL_NAMES: {
|
|
68
|
+
/** Mark issues as test fixtures (not for manual processing) */
|
|
69
|
+
readonly TEST_FIXTURE: "test-fixture";
|
|
70
|
+
/** Identify issues created by E2E tests */
|
|
71
|
+
readonly E2E_TEST: "e2e-test";
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Categories of environment issues that agents can report
|
|
75
|
+
*/
|
|
76
|
+
export declare const ENVIRONMENT_ISSUE_TYPES: {
|
|
77
|
+
readonly PERMISSION: "permission";
|
|
78
|
+
readonly NETWORK: "network";
|
|
79
|
+
readonly SANDBOX: "sandbox";
|
|
80
|
+
readonly LINEAR_CLI: "linear-cli";
|
|
81
|
+
readonly DEPENDENCY: "dependency";
|
|
82
|
+
readonly TIMEOUT: "timeout";
|
|
83
|
+
readonly TOOL: "tool";
|
|
84
|
+
readonly HUMAN_BLOCKER: "human-blocker";
|
|
85
|
+
};
|
|
86
|
+
export type EnvironmentIssueType = (typeof ENVIRONMENT_ISSUE_TYPES)[keyof typeof ENVIRONMENT_ISSUE_TYPES];
|
|
87
|
+
//# sourceMappingURL=constants.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/constants.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,QAAQ,CAAA;AAE9C;;GAEG;AACH,eAAO,MAAM,iBAAiB,gCAAgC,CAAA;AAE9D;;GAEG;AACH,eAAO,MAAM,uBAAuB,KAAK,CAAA;AAEzC;;GAEG;AACH,eAAO,MAAM,gBAAgB,MAAM,CAAA;AAEnC;;GAEG;AACH,eAAO,MAAM,mBAAmB,uCAAuC,CAAA;AAQvE;;;;;;GAMG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED;;;;;GAKG;AACH,eAAO,MAAM,eAAe;;;IAG1B,mDAAmD;;CAE3C,CAAA;AAEV;;;;;;GAMG;AACH,eAAO,MAAM,aAAa;;;;;CAKhB,CAAA;AAGV,eAAO,MAAM,gBAAgB;IAC3B,+DAA+D;;IAE/D,2CAA2C;;CAEnC,CAAA;AAMV;;GAEG;AACH,eAAO,MAAM,uBAAuB;;;;;;;;;CAS1B,CAAA;AAEV,MAAM,MAAM,oBAAoB,GAC9B,CAAC,OAAO,uBAAuB,CAAC,CAAC,MAAM,OAAO,uBAAuB,CAAC,CAAA"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linear API Constants
|
|
3
|
+
*
|
|
4
|
+
* Contains API limits, well-known IDs, and configuration values.
|
|
5
|
+
* All workspace-specific IDs must be provided via environment variables.
|
|
6
|
+
*/
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// LINEAR API LIMITS
|
|
9
|
+
// ============================================================================
|
|
10
|
+
/**
|
|
11
|
+
* Maximum length for comment body.
|
|
12
|
+
* Linear doesn't publicly document this limit, but testing shows ~10k is safe.
|
|
13
|
+
* We use a conservative limit to avoid truncation issues.
|
|
14
|
+
*/
|
|
15
|
+
export const LINEAR_COMMENT_MAX_LENGTH = 10000;
|
|
16
|
+
/**
|
|
17
|
+
* Marker appended when content is truncated
|
|
18
|
+
*/
|
|
19
|
+
export const TRUNCATION_MARKER = '\n\n... (content truncated)';
|
|
20
|
+
/**
|
|
21
|
+
* Maximum number of comments for a single completion (safety limit)
|
|
22
|
+
*/
|
|
23
|
+
export const MAX_COMPLETION_COMMENTS = 10;
|
|
24
|
+
/**
|
|
25
|
+
* Characters reserved for part markers and overhead
|
|
26
|
+
*/
|
|
27
|
+
export const COMMENT_OVERHEAD = 100;
|
|
28
|
+
/**
|
|
29
|
+
* Continuation marker for multi-part comments
|
|
30
|
+
*/
|
|
31
|
+
export const CONTINUATION_MARKER = '\n\n*...continued in next comment*';
|
|
32
|
+
// ============================================================================
|
|
33
|
+
// WELL-KNOWN LINEAR IDs
|
|
34
|
+
// All workspace-specific IDs are loaded from environment variables.
|
|
35
|
+
// No hardcoded fallback UUIDs — configure via env vars.
|
|
36
|
+
// ============================================================================
|
|
37
|
+
/**
|
|
38
|
+
* Default team UUID
|
|
39
|
+
* Must be set via LINEAR_TEAM_ID env var
|
|
40
|
+
*
|
|
41
|
+
* Uses a getter to read lazily from process.env, avoiding ESM import
|
|
42
|
+
* hoisting issues where the value is captured before dotenv runs.
|
|
43
|
+
*/
|
|
44
|
+
export function getDefaultTeamId() {
|
|
45
|
+
return process.env.LINEAR_TEAM_ID ?? '';
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Default team name
|
|
49
|
+
* Can be set via LINEAR_TEAM_NAME env var (auto-set by orchestrator from issue context)
|
|
50
|
+
*/
|
|
51
|
+
export function getDefaultTeamName() {
|
|
52
|
+
return process.env.LINEAR_TEAM_NAME ?? '';
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Project IDs — must be set via env vars:
|
|
56
|
+
* - LINEAR_PROJECT_AGENT
|
|
57
|
+
* - LINEAR_PROJECT_SOCIAL
|
|
58
|
+
* - LINEAR_PROJECT_TEST
|
|
59
|
+
*/
|
|
60
|
+
export const LINEAR_PROJECTS = {
|
|
61
|
+
get AGENT() { return process.env.LINEAR_PROJECT_AGENT ?? ''; },
|
|
62
|
+
get SOCIAL() { return process.env.LINEAR_PROJECT_SOCIAL ?? ''; },
|
|
63
|
+
/** Test project for E2E testing of orchestrator */
|
|
64
|
+
get TEST() { return process.env.LINEAR_PROJECT_TEST ?? ''; },
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Label IDs for issue classification
|
|
68
|
+
* Must be set via env vars:
|
|
69
|
+
* - LINEAR_LABEL_BUG
|
|
70
|
+
* - LINEAR_LABEL_FEATURE
|
|
71
|
+
* - LINEAR_LABEL_CHORE
|
|
72
|
+
*/
|
|
73
|
+
export const LINEAR_LABELS = {
|
|
74
|
+
get BUG() { return process.env.LINEAR_LABEL_BUG ?? ''; },
|
|
75
|
+
get FEATURE() { return process.env.LINEAR_LABEL_FEATURE ?? ''; },
|
|
76
|
+
get CHORE() { return process.env.LINEAR_LABEL_CHORE ?? ''; },
|
|
77
|
+
get NEEDS_HUMAN() { return process.env.LINEAR_LABEL_NEEDS_HUMAN ?? ''; },
|
|
78
|
+
};
|
|
79
|
+
// Test-related labels (created dynamically, not hardcoded IDs)
|
|
80
|
+
export const TEST_LABEL_NAMES = {
|
|
81
|
+
/** Mark issues as test fixtures (not for manual processing) */
|
|
82
|
+
TEST_FIXTURE: 'test-fixture',
|
|
83
|
+
/** Identify issues created by E2E tests */
|
|
84
|
+
E2E_TEST: 'e2e-test',
|
|
85
|
+
};
|
|
86
|
+
// ============================================================================
|
|
87
|
+
// ENVIRONMENT ISSUE REPORTING
|
|
88
|
+
// ============================================================================
|
|
89
|
+
/**
|
|
90
|
+
* Categories of environment issues that agents can report
|
|
91
|
+
*/
|
|
92
|
+
export const ENVIRONMENT_ISSUE_TYPES = {
|
|
93
|
+
PERMISSION: 'permission',
|
|
94
|
+
NETWORK: 'network',
|
|
95
|
+
SANDBOX: 'sandbox',
|
|
96
|
+
LINEAR_CLI: 'linear-cli',
|
|
97
|
+
DEPENDENCY: 'dependency',
|
|
98
|
+
TIMEOUT: 'timeout',
|
|
99
|
+
TOOL: 'tool',
|
|
100
|
+
HUMAN_BLOCKER: 'human-blocker',
|
|
101
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default auto-trigger configuration parser.
|
|
3
|
+
*
|
|
4
|
+
* Parses environment variables to determine which automated
|
|
5
|
+
* workflows (QA, acceptance) should be triggered on status transitions.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Auto-trigger configuration shape.
|
|
9
|
+
* Matches the AutoTriggerConfig interface from @renseiai/agentfactory-nextjs.
|
|
10
|
+
*/
|
|
11
|
+
export interface DefaultAutoTriggerConfig {
|
|
12
|
+
enableAutoQA: boolean;
|
|
13
|
+
enableAutoAcceptance: boolean;
|
|
14
|
+
autoQARequireAgentWorked: boolean;
|
|
15
|
+
autoAcceptanceRequireAgentWorked: boolean;
|
|
16
|
+
autoQAProjects: string[];
|
|
17
|
+
autoAcceptanceProjects: string[];
|
|
18
|
+
autoQAExcludeLabels: string[];
|
|
19
|
+
autoAcceptanceExcludeLabels: string[];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Parse auto-trigger configuration from environment variables.
|
|
23
|
+
*
|
|
24
|
+
* Environment variables:
|
|
25
|
+
* ENABLE_AUTO_QA - Enable automatic QA on Finished transition
|
|
26
|
+
* ENABLE_AUTO_ACCEPTANCE - Enable automatic acceptance on Delivered transition
|
|
27
|
+
* AUTO_QA_REQUIRE_AGENT_WORKED - Only auto-QA agent-worked issues (default: true)
|
|
28
|
+
* AUTO_ACCEPTANCE_REQUIRE_AGENT_WORKED - Only auto-accept agent-worked issues (default: true)
|
|
29
|
+
* AUTO_QA_PROJECTS - Comma-separated project names to auto-QA
|
|
30
|
+
* AUTO_ACCEPTANCE_PROJECTS - Comma-separated project names to auto-accept
|
|
31
|
+
* AUTO_QA_EXCLUDE_LABELS - Labels that exclude issues from auto-QA
|
|
32
|
+
* AUTO_ACCEPTANCE_EXCLUDE_LABELS - Labels that exclude issues from auto-acceptance
|
|
33
|
+
*/
|
|
34
|
+
export declare function defaultParseAutoTriggerConfig(): DefaultAutoTriggerConfig;
|
|
35
|
+
//# sourceMappingURL=auto-trigger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auto-trigger.d.ts","sourceRoot":"","sources":["../../../src/defaults/auto-trigger.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;GAGG;AACH,MAAM,WAAW,wBAAwB;IACvC,YAAY,EAAE,OAAO,CAAA;IACrB,oBAAoB,EAAE,OAAO,CAAA;IAC7B,wBAAwB,EAAE,OAAO,CAAA;IACjC,gCAAgC,EAAE,OAAO,CAAA;IACzC,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,sBAAsB,EAAE,MAAM,EAAE,CAAA;IAChC,mBAAmB,EAAE,MAAM,EAAE,CAAA;IAC7B,2BAA2B,EAAE,MAAM,EAAE,CAAA;CACtC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,6BAA6B,IAAI,wBAAwB,CAWxE"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default auto-trigger configuration parser.
|
|
3
|
+
*
|
|
4
|
+
* Parses environment variables to determine which automated
|
|
5
|
+
* workflows (QA, acceptance) should be triggered on status transitions.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Parse auto-trigger configuration from environment variables.
|
|
9
|
+
*
|
|
10
|
+
* Environment variables:
|
|
11
|
+
* ENABLE_AUTO_QA - Enable automatic QA on Finished transition
|
|
12
|
+
* ENABLE_AUTO_ACCEPTANCE - Enable automatic acceptance on Delivered transition
|
|
13
|
+
* AUTO_QA_REQUIRE_AGENT_WORKED - Only auto-QA agent-worked issues (default: true)
|
|
14
|
+
* AUTO_ACCEPTANCE_REQUIRE_AGENT_WORKED - Only auto-accept agent-worked issues (default: true)
|
|
15
|
+
* AUTO_QA_PROJECTS - Comma-separated project names to auto-QA
|
|
16
|
+
* AUTO_ACCEPTANCE_PROJECTS - Comma-separated project names to auto-accept
|
|
17
|
+
* AUTO_QA_EXCLUDE_LABELS - Labels that exclude issues from auto-QA
|
|
18
|
+
* AUTO_ACCEPTANCE_EXCLUDE_LABELS - Labels that exclude issues from auto-acceptance
|
|
19
|
+
*/
|
|
20
|
+
export function defaultParseAutoTriggerConfig() {
|
|
21
|
+
return {
|
|
22
|
+
enableAutoQA: process.env.ENABLE_AUTO_QA === 'true',
|
|
23
|
+
enableAutoAcceptance: process.env.ENABLE_AUTO_ACCEPTANCE === 'true',
|
|
24
|
+
autoQARequireAgentWorked: process.env.AUTO_QA_REQUIRE_AGENT_WORKED !== 'false',
|
|
25
|
+
autoAcceptanceRequireAgentWorked: process.env.AUTO_ACCEPTANCE_REQUIRE_AGENT_WORKED !== 'false',
|
|
26
|
+
autoQAProjects: parseCommaSeparated(process.env.AUTO_QA_PROJECTS),
|
|
27
|
+
autoAcceptanceProjects: parseCommaSeparated(process.env.AUTO_ACCEPTANCE_PROJECTS),
|
|
28
|
+
autoQAExcludeLabels: parseCommaSeparated(process.env.AUTO_QA_EXCLUDE_LABELS),
|
|
29
|
+
autoAcceptanceExcludeLabels: parseCommaSeparated(process.env.AUTO_ACCEPTANCE_EXCLUDE_LABELS),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function parseCommaSeparated(value) {
|
|
33
|
+
if (!value)
|
|
34
|
+
return [];
|
|
35
|
+
return value.split(',').map(s => s.trim()).filter(Boolean);
|
|
36
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default implementations for prompt generation, work type detection,
|
|
3
|
+
* priority assignment, and auto-trigger configuration.
|
|
4
|
+
*
|
|
5
|
+
* New users start with these defaults and customize as needed.
|
|
6
|
+
* Rensei overrides these with its own prompts.ts.
|
|
7
|
+
*/
|
|
8
|
+
export { defaultGeneratePrompt, defaultBuildParentQAContext, defaultBuildParentAcceptanceContext, buildFailureContextBlock, WORK_RESULT_MARKER_INSTRUCTION, PR_SELECTION_GUIDANCE, type WorkflowContext, } from './prompts.js';
|
|
9
|
+
export { defaultDetectWorkTypeFromPrompt } from './work-type-detection.js';
|
|
10
|
+
export { defaultGetPriority } from './priority.js';
|
|
11
|
+
export { defaultParseAutoTriggerConfig, type DefaultAutoTriggerConfig, } from './auto-trigger.js';
|
|
12
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/defaults/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EACL,qBAAqB,EACrB,2BAA2B,EAC3B,mCAAmC,EACnC,wBAAwB,EACxB,8BAA8B,EAC9B,qBAAqB,EACrB,KAAK,eAAe,GACrB,MAAM,cAAc,CAAA;AAErB,OAAO,EAAE,+BAA+B,EAAE,MAAM,0BAA0B,CAAA;AAE1E,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAA;AAElD,OAAO,EACL,6BAA6B,EAC7B,KAAK,wBAAwB,GAC9B,MAAM,mBAAmB,CAAA"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default implementations for prompt generation, work type detection,
|
|
3
|
+
* priority assignment, and auto-trigger configuration.
|
|
4
|
+
*
|
|
5
|
+
* New users start with these defaults and customize as needed.
|
|
6
|
+
* Rensei overrides these with its own prompts.ts.
|
|
7
|
+
*/
|
|
8
|
+
export { defaultGeneratePrompt, defaultBuildParentQAContext, defaultBuildParentAcceptanceContext, buildFailureContextBlock, WORK_RESULT_MARKER_INSTRUCTION, PR_SELECTION_GUIDANCE, } from './prompts.js';
|
|
9
|
+
export { defaultDetectWorkTypeFromPrompt } from './work-type-detection.js';
|
|
10
|
+
export { defaultGetPriority } from './priority.js';
|
|
11
|
+
export { defaultParseAutoTriggerConfig, } from './auto-trigger.js';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default priority values for each work type.
|
|
3
|
+
*
|
|
4
|
+
* Lower values = higher priority in the work queue.
|
|
5
|
+
*/
|
|
6
|
+
import type { AgentWorkType } from '../types.js';
|
|
7
|
+
/**
|
|
8
|
+
* Get the default priority for a work type.
|
|
9
|
+
*
|
|
10
|
+
* Priority scale:
|
|
11
|
+
* 1 = Urgent (reserved for future use)
|
|
12
|
+
* 2 = High (QA, acceptance, coordination, inflight, refinement)
|
|
13
|
+
* 3 = Normal (development, backlog-creation)
|
|
14
|
+
* 4 = Low (research)
|
|
15
|
+
*
|
|
16
|
+
* @param workType - The work type to get priority for
|
|
17
|
+
* @returns Priority value (lower = higher priority)
|
|
18
|
+
*/
|
|
19
|
+
export declare function defaultGetPriority(workType: AgentWorkType): number;
|
|
20
|
+
//# sourceMappingURL=priority.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"priority.d.ts","sourceRoot":"","sources":["../../../src/defaults/priority.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAEhD;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,aAAa,GAAG,MAAM,CAmBlE"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default priority values for each work type.
|
|
3
|
+
*
|
|
4
|
+
* Lower values = higher priority in the work queue.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Get the default priority for a work type.
|
|
8
|
+
*
|
|
9
|
+
* Priority scale:
|
|
10
|
+
* 1 = Urgent (reserved for future use)
|
|
11
|
+
* 2 = High (QA, acceptance, coordination, inflight, refinement)
|
|
12
|
+
* 3 = Normal (development, backlog-creation)
|
|
13
|
+
* 4 = Low (research)
|
|
14
|
+
*
|
|
15
|
+
* @param workType - The work type to get priority for
|
|
16
|
+
* @returns Priority value (lower = higher priority)
|
|
17
|
+
*/
|
|
18
|
+
export function defaultGetPriority(workType) {
|
|
19
|
+
switch (workType) {
|
|
20
|
+
case 'qa':
|
|
21
|
+
case 'acceptance':
|
|
22
|
+
case 'refinement':
|
|
23
|
+
case 'refinement-coordination':
|
|
24
|
+
case 'inflight':
|
|
25
|
+
case 'coordination':
|
|
26
|
+
case 'qa-coordination':
|
|
27
|
+
case 'acceptance-coordination':
|
|
28
|
+
return 2;
|
|
29
|
+
case 'backlog-creation':
|
|
30
|
+
case 'development':
|
|
31
|
+
return 3;
|
|
32
|
+
case 'research':
|
|
33
|
+
return 4;
|
|
34
|
+
default:
|
|
35
|
+
return 3;
|
|
36
|
+
}
|
|
37
|
+
}
|