@renseiai/plugin-linear 0.8.6
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/__tests__/subpath-exports.test.d.ts +2 -0
- package/dist/src/__tests__/subpath-exports.test.d.ts.map +1 -0
- package/dist/src/__tests__/subpath-exports.test.js +136 -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 +38 -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 +313 -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 +98 -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 +32 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +35 -0
- package/dist/src/issue-tracker-adapter.d.ts +113 -0
- package/dist/src/issue-tracker-adapter.d.ts.map +1 -0
- package/dist/src/issue-tracker-adapter.js +169 -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/retry.test.d.ts +2 -0
- package/dist/src/retry.test.d.ts.map +1 -0
- package/dist/src/retry.test.js +266 -0
- package/dist/src/tools/deployment-bridge.d.ts +34 -0
- package/dist/src/tools/deployment-bridge.d.ts.map +1 -0
- package/dist/src/tools/deployment-bridge.js +122 -0
- package/dist/src/tools/linear-plugin.d.ts +23 -0
- package/dist/src/tools/linear-plugin.d.ts.map +1 -0
- package/dist/src/tools/linear-plugin.js +175 -0
- package/dist/src/tools/linear-runner.d.ts +37 -0
- package/dist/src/tools/linear-runner.d.ts.map +1 -0
- package/dist/src/tools/linear-runner.js +810 -0
- package/dist/src/types.d.ts +492 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +148 -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 +73 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { RetryConfig } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Default retry configuration
|
|
4
|
+
*/
|
|
5
|
+
export declare const DEFAULT_RETRY_CONFIG: Required<RetryConfig>;
|
|
6
|
+
/**
|
|
7
|
+
* Sleep utility for async delays
|
|
8
|
+
*/
|
|
9
|
+
export declare function sleep(ms: number): Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* Calculate delay for a given retry attempt with exponential backoff
|
|
12
|
+
*/
|
|
13
|
+
export declare function calculateDelay(attempt: number, config: Required<RetryConfig>): number;
|
|
14
|
+
/**
|
|
15
|
+
* Retry context passed to callbacks
|
|
16
|
+
*/
|
|
17
|
+
export interface RetryContext {
|
|
18
|
+
attempt: number;
|
|
19
|
+
maxRetries: number;
|
|
20
|
+
lastError?: Error;
|
|
21
|
+
delay: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Callback for retry events
|
|
25
|
+
*/
|
|
26
|
+
export type RetryCallback = (context: RetryContext) => void;
|
|
27
|
+
/**
|
|
28
|
+
* Options for withRetry function
|
|
29
|
+
*/
|
|
30
|
+
export interface WithRetryOptions {
|
|
31
|
+
config?: RetryConfig;
|
|
32
|
+
onRetry?: RetryCallback;
|
|
33
|
+
shouldRetry?: (error: unknown) => boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Optional callback to extract a rate-limit delay (in ms) from an error.
|
|
36
|
+
* When provided and returns a positive number, that delay is used instead
|
|
37
|
+
* of the standard exponential backoff for that retry attempt.
|
|
38
|
+
*/
|
|
39
|
+
getRetryAfterMs?: (error: unknown) => number | null;
|
|
40
|
+
/**
|
|
41
|
+
* Optional callback invoked when a rate limit is detected (getRetryAfterMs
|
|
42
|
+
* returned a value). Use this to penalize a shared token bucket so other
|
|
43
|
+
* concurrent callers also back off.
|
|
44
|
+
*/
|
|
45
|
+
onRateLimited?: (retryAfterMs: number) => void;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Execute an async function with exponential backoff retry logic.
|
|
49
|
+
*
|
|
50
|
+
* When `getRetryAfterMs` is provided and returns a positive delay for an
|
|
51
|
+
* error, that delay is used instead of exponential backoff. This allows
|
|
52
|
+
* honoring HTTP 429 Retry-After headers from upstream APIs.
|
|
53
|
+
*/
|
|
54
|
+
export declare function withRetry<T>(fn: () => Promise<T>, options?: WithRetryOptions): Promise<T>;
|
|
55
|
+
/**
|
|
56
|
+
* Create a retry wrapper with pre-configured options
|
|
57
|
+
*/
|
|
58
|
+
export declare function createRetryWrapper(defaultOptions?: WithRetryOptions): <T>(fn: () => Promise<T>, options?: WithRetryOptions) => Promise<T>;
|
|
59
|
+
//# sourceMappingURL=retry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../../src/retry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAG7C;;GAEG;AACH,eAAO,MAAM,oBAAoB,EAAE,QAAQ,CAAC,WAAW,CAMtD,CAAA;AAED;;GAEG;AACH,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE/C;AAED;;GAEG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,QAAQ,CAAC,WAAW,CAAC,GAC5B,MAAM,CAIR;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,KAAK,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;CACd;AAED;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,YAAY,KAAK,IAAI,CAAA;AAE3D;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB,OAAO,CAAC,EAAE,aAAa,CAAA;IACvB,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAA;IACzC;;;;OAIG;IACH,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,MAAM,GAAG,IAAI,CAAA;IACnD;;;;OAIG;IACH,aAAa,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,IAAI,CAAA;CAC/C;AAED;;;;;;GAMG;AACH,wBAAsB,SAAS,CAAC,CAAC,EAC/B,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,CAAC,CAAC,CA4CZ;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,cAAc,GAAE,gBAAqB,IACrD,CAAC,EAChB,IAAI,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,UAAU,gBAAgB,KACzB,OAAO,CAAC,CAAC,CAAC,CAUd"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { isRetryableError } from './errors.js';
|
|
2
|
+
/**
|
|
3
|
+
* Default retry configuration
|
|
4
|
+
*/
|
|
5
|
+
export const DEFAULT_RETRY_CONFIG = {
|
|
6
|
+
maxRetries: 3,
|
|
7
|
+
initialDelayMs: 1000,
|
|
8
|
+
backoffMultiplier: 2,
|
|
9
|
+
maxDelayMs: 10000,
|
|
10
|
+
retryableStatusCodes: [429, 500, 502, 503, 504],
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Sleep utility for async delays
|
|
14
|
+
*/
|
|
15
|
+
export function sleep(ms) {
|
|
16
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Calculate delay for a given retry attempt with exponential backoff
|
|
20
|
+
*/
|
|
21
|
+
export function calculateDelay(attempt, config) {
|
|
22
|
+
const delay = config.initialDelayMs * Math.pow(config.backoffMultiplier, attempt);
|
|
23
|
+
return Math.min(delay, config.maxDelayMs);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Execute an async function with exponential backoff retry logic.
|
|
27
|
+
*
|
|
28
|
+
* When `getRetryAfterMs` is provided and returns a positive delay for an
|
|
29
|
+
* error, that delay is used instead of exponential backoff. This allows
|
|
30
|
+
* honoring HTTP 429 Retry-After headers from upstream APIs.
|
|
31
|
+
*/
|
|
32
|
+
export async function withRetry(fn, options = {}) {
|
|
33
|
+
const config = {
|
|
34
|
+
...DEFAULT_RETRY_CONFIG,
|
|
35
|
+
...options.config,
|
|
36
|
+
};
|
|
37
|
+
const shouldRetry = options.shouldRetry ??
|
|
38
|
+
((error) => isRetryableError(error, config.retryableStatusCodes));
|
|
39
|
+
let lastError = new Error('Unknown error');
|
|
40
|
+
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
|
|
41
|
+
try {
|
|
42
|
+
return await fn();
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
46
|
+
if (attempt === config.maxRetries || !shouldRetry(error)) {
|
|
47
|
+
throw lastError;
|
|
48
|
+
}
|
|
49
|
+
// Check for rate-limit-specific delay (Retry-After)
|
|
50
|
+
const retryAfterMs = options.getRetryAfterMs?.(error) ?? null;
|
|
51
|
+
const delay = retryAfterMs ?? calculateDelay(attempt, config);
|
|
52
|
+
if (retryAfterMs !== null && options.onRateLimited) {
|
|
53
|
+
options.onRateLimited(retryAfterMs);
|
|
54
|
+
}
|
|
55
|
+
if (options.onRetry) {
|
|
56
|
+
options.onRetry({
|
|
57
|
+
attempt,
|
|
58
|
+
maxRetries: config.maxRetries,
|
|
59
|
+
lastError,
|
|
60
|
+
delay,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
await sleep(delay);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
throw lastError;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Create a retry wrapper with pre-configured options
|
|
70
|
+
*/
|
|
71
|
+
export function createRetryWrapper(defaultOptions = {}) {
|
|
72
|
+
return function (fn, options) {
|
|
73
|
+
return withRetry(fn, {
|
|
74
|
+
...defaultOptions,
|
|
75
|
+
...options,
|
|
76
|
+
config: {
|
|
77
|
+
...defaultOptions.config,
|
|
78
|
+
...options?.config,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry.test.d.ts","sourceRoot":"","sources":["../../src/retry.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { calculateDelay, withRetry, createRetryWrapper, DEFAULT_RETRY_CONFIG, } from './retry.js';
|
|
3
|
+
// Use very short delays to avoid real waits in tests
|
|
4
|
+
const fastConfig = {
|
|
5
|
+
maxRetries: 3,
|
|
6
|
+
initialDelayMs: 1,
|
|
7
|
+
backoffMultiplier: 2,
|
|
8
|
+
maxDelayMs: 1,
|
|
9
|
+
retryableStatusCodes: [429, 500, 502, 503, 504],
|
|
10
|
+
};
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// calculateDelay
|
|
13
|
+
// ============================================================================
|
|
14
|
+
describe('calculateDelay', () => {
|
|
15
|
+
const config = {
|
|
16
|
+
maxRetries: 3,
|
|
17
|
+
initialDelayMs: 1000,
|
|
18
|
+
backoffMultiplier: 2,
|
|
19
|
+
maxDelayMs: 10000,
|
|
20
|
+
retryableStatusCodes: [429, 500, 502, 503, 504],
|
|
21
|
+
};
|
|
22
|
+
it('returns initialDelayMs for attempt 0', () => {
|
|
23
|
+
expect(calculateDelay(0, config)).toBe(1000);
|
|
24
|
+
});
|
|
25
|
+
it('returns initialDelayMs * backoffMultiplier for attempt 1', () => {
|
|
26
|
+
expect(calculateDelay(1, config)).toBe(2000);
|
|
27
|
+
});
|
|
28
|
+
it('returns initialDelayMs * backoffMultiplier^2 for attempt 2', () => {
|
|
29
|
+
expect(calculateDelay(2, config)).toBe(4000);
|
|
30
|
+
});
|
|
31
|
+
it('caps at maxDelayMs', () => {
|
|
32
|
+
// attempt 4 would be 1000 * 2^4 = 16000, but max is 10000
|
|
33
|
+
expect(calculateDelay(4, config)).toBe(10000);
|
|
34
|
+
});
|
|
35
|
+
it('works with custom config values', () => {
|
|
36
|
+
const custom = {
|
|
37
|
+
maxRetries: 5,
|
|
38
|
+
initialDelayMs: 500,
|
|
39
|
+
backoffMultiplier: 3,
|
|
40
|
+
maxDelayMs: 20000,
|
|
41
|
+
retryableStatusCodes: [],
|
|
42
|
+
};
|
|
43
|
+
// attempt 0: 500
|
|
44
|
+
expect(calculateDelay(0, custom)).toBe(500);
|
|
45
|
+
// attempt 1: 500 * 3 = 1500
|
|
46
|
+
expect(calculateDelay(1, custom)).toBe(1500);
|
|
47
|
+
// attempt 2: 500 * 9 = 4500
|
|
48
|
+
expect(calculateDelay(2, custom)).toBe(4500);
|
|
49
|
+
// attempt 3: 500 * 27 = 13500
|
|
50
|
+
expect(calculateDelay(3, custom)).toBe(13500);
|
|
51
|
+
// attempt 4: 500 * 81 = 40500, capped at 20000
|
|
52
|
+
expect(calculateDelay(4, custom)).toBe(20000);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
// ============================================================================
|
|
56
|
+
// withRetry
|
|
57
|
+
// ============================================================================
|
|
58
|
+
describe('withRetry', () => {
|
|
59
|
+
it('succeeds on first attempt without retrying', async () => {
|
|
60
|
+
const fn = vi.fn().mockResolvedValue('ok');
|
|
61
|
+
const result = await withRetry(fn, { config: fastConfig });
|
|
62
|
+
expect(result).toBe('ok');
|
|
63
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
64
|
+
});
|
|
65
|
+
it('succeeds on 2nd attempt after 1 failure', async () => {
|
|
66
|
+
const fn = vi
|
|
67
|
+
.fn()
|
|
68
|
+
.mockRejectedValueOnce(new Error('ECONNRESET'))
|
|
69
|
+
.mockResolvedValue('ok');
|
|
70
|
+
const result = await withRetry(fn, {
|
|
71
|
+
config: fastConfig,
|
|
72
|
+
shouldRetry: () => true,
|
|
73
|
+
});
|
|
74
|
+
expect(result).toBe('ok');
|
|
75
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
76
|
+
});
|
|
77
|
+
it('succeeds on nth attempt', async () => {
|
|
78
|
+
const fn = vi
|
|
79
|
+
.fn()
|
|
80
|
+
.mockRejectedValueOnce(new Error('fail 1'))
|
|
81
|
+
.mockRejectedValueOnce(new Error('fail 2'))
|
|
82
|
+
.mockRejectedValueOnce(new Error('fail 3'))
|
|
83
|
+
.mockResolvedValue('ok');
|
|
84
|
+
const result = await withRetry(fn, {
|
|
85
|
+
config: fastConfig,
|
|
86
|
+
shouldRetry: () => true,
|
|
87
|
+
});
|
|
88
|
+
expect(result).toBe('ok');
|
|
89
|
+
expect(fn).toHaveBeenCalledTimes(4);
|
|
90
|
+
});
|
|
91
|
+
it('throws after exhausting all retries', async () => {
|
|
92
|
+
const fn = vi.fn().mockRejectedValue(new Error('persistent failure'));
|
|
93
|
+
await expect(withRetry(fn, {
|
|
94
|
+
config: { ...fastConfig, maxRetries: 2 },
|
|
95
|
+
shouldRetry: () => true,
|
|
96
|
+
})).rejects.toThrow('persistent failure');
|
|
97
|
+
// attempt 0, 1, 2 = 3 calls total
|
|
98
|
+
expect(fn).toHaveBeenCalledTimes(3);
|
|
99
|
+
});
|
|
100
|
+
it('calls onRetry callback on each retry', async () => {
|
|
101
|
+
const onRetry = vi.fn();
|
|
102
|
+
const fn = vi
|
|
103
|
+
.fn()
|
|
104
|
+
.mockRejectedValueOnce(new Error('fail 1'))
|
|
105
|
+
.mockRejectedValueOnce(new Error('fail 2'))
|
|
106
|
+
.mockResolvedValue('ok');
|
|
107
|
+
await withRetry(fn, {
|
|
108
|
+
config: fastConfig,
|
|
109
|
+
shouldRetry: () => true,
|
|
110
|
+
onRetry,
|
|
111
|
+
});
|
|
112
|
+
expect(onRetry).toHaveBeenCalledTimes(2);
|
|
113
|
+
// First retry: attempt 0
|
|
114
|
+
expect(onRetry).toHaveBeenNthCalledWith(1, {
|
|
115
|
+
attempt: 0,
|
|
116
|
+
maxRetries: fastConfig.maxRetries,
|
|
117
|
+
lastError: expect.objectContaining({ message: 'fail 1' }),
|
|
118
|
+
delay: expect.any(Number),
|
|
119
|
+
});
|
|
120
|
+
// Second retry: attempt 1
|
|
121
|
+
expect(onRetry).toHaveBeenNthCalledWith(2, {
|
|
122
|
+
attempt: 1,
|
|
123
|
+
maxRetries: fastConfig.maxRetries,
|
|
124
|
+
lastError: expect.objectContaining({ message: 'fail 2' }),
|
|
125
|
+
delay: expect.any(Number),
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
it('respects custom shouldRetry predicate and stops retrying when false', async () => {
|
|
129
|
+
const fn = vi
|
|
130
|
+
.fn()
|
|
131
|
+
.mockRejectedValueOnce(new Error('retryable'))
|
|
132
|
+
.mockRejectedValueOnce(new Error('not retryable'))
|
|
133
|
+
.mockResolvedValue('ok');
|
|
134
|
+
const shouldRetry = vi
|
|
135
|
+
.fn()
|
|
136
|
+
.mockReturnValueOnce(true)
|
|
137
|
+
.mockReturnValueOnce(false);
|
|
138
|
+
await expect(withRetry(fn, { config: fastConfig, shouldRetry })).rejects.toThrow('not retryable');
|
|
139
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
140
|
+
expect(shouldRetry).toHaveBeenCalledTimes(2);
|
|
141
|
+
});
|
|
142
|
+
it('does not retry when shouldRetry returns false for specific error', async () => {
|
|
143
|
+
const fn = vi.fn().mockRejectedValue(new Error('fatal'));
|
|
144
|
+
await expect(withRetry(fn, {
|
|
145
|
+
config: fastConfig,
|
|
146
|
+
shouldRetry: () => false,
|
|
147
|
+
})).rejects.toThrow('fatal');
|
|
148
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
149
|
+
});
|
|
150
|
+
it('uses getRetryAfterMs delay when provided', async () => {
|
|
151
|
+
const onRetry = vi.fn();
|
|
152
|
+
const fn = vi
|
|
153
|
+
.fn()
|
|
154
|
+
.mockRejectedValueOnce(new Error('rate limited'))
|
|
155
|
+
.mockResolvedValue('ok');
|
|
156
|
+
await withRetry(fn, {
|
|
157
|
+
config: fastConfig,
|
|
158
|
+
shouldRetry: () => true,
|
|
159
|
+
getRetryAfterMs: () => 5,
|
|
160
|
+
onRetry,
|
|
161
|
+
});
|
|
162
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
163
|
+
// The delay should be the value from getRetryAfterMs (5), not the calculated backoff
|
|
164
|
+
expect(onRetry).toHaveBeenCalledWith(expect.objectContaining({ delay: 5 }));
|
|
165
|
+
});
|
|
166
|
+
it('calls onRateLimited when getRetryAfterMs returns a value', async () => {
|
|
167
|
+
const onRateLimited = vi.fn();
|
|
168
|
+
const fn = vi
|
|
169
|
+
.fn()
|
|
170
|
+
.mockRejectedValueOnce(new Error('rate limited'))
|
|
171
|
+
.mockResolvedValue('ok');
|
|
172
|
+
await withRetry(fn, {
|
|
173
|
+
config: fastConfig,
|
|
174
|
+
shouldRetry: () => true,
|
|
175
|
+
getRetryAfterMs: () => 42,
|
|
176
|
+
onRateLimited,
|
|
177
|
+
});
|
|
178
|
+
expect(onRateLimited).toHaveBeenCalledTimes(1);
|
|
179
|
+
expect(onRateLimited).toHaveBeenCalledWith(42);
|
|
180
|
+
});
|
|
181
|
+
it('does not call onRateLimited when getRetryAfterMs returns null', async () => {
|
|
182
|
+
const onRateLimited = vi.fn();
|
|
183
|
+
const fn = vi
|
|
184
|
+
.fn()
|
|
185
|
+
.mockRejectedValueOnce(new Error('network error'))
|
|
186
|
+
.mockResolvedValue('ok');
|
|
187
|
+
await withRetry(fn, {
|
|
188
|
+
config: fastConfig,
|
|
189
|
+
shouldRetry: () => true,
|
|
190
|
+
getRetryAfterMs: () => null,
|
|
191
|
+
onRateLimited,
|
|
192
|
+
});
|
|
193
|
+
expect(onRateLimited).not.toHaveBeenCalled();
|
|
194
|
+
});
|
|
195
|
+
it('wraps non-Error thrown values into Error objects', async () => {
|
|
196
|
+
const fn = vi.fn().mockRejectedValue('string error');
|
|
197
|
+
await expect(withRetry(fn, {
|
|
198
|
+
config: { ...fastConfig, maxRetries: 0 },
|
|
199
|
+
shouldRetry: () => true,
|
|
200
|
+
})).rejects.toThrow('string error');
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
// ============================================================================
|
|
204
|
+
// createRetryWrapper
|
|
205
|
+
// ============================================================================
|
|
206
|
+
describe('createRetryWrapper', () => {
|
|
207
|
+
it('creates a wrapper that uses default options', async () => {
|
|
208
|
+
const onRetry = vi.fn();
|
|
209
|
+
const retryFn = createRetryWrapper({
|
|
210
|
+
config: fastConfig,
|
|
211
|
+
shouldRetry: () => true,
|
|
212
|
+
onRetry,
|
|
213
|
+
});
|
|
214
|
+
const fn = vi
|
|
215
|
+
.fn()
|
|
216
|
+
.mockRejectedValueOnce(new Error('fail'))
|
|
217
|
+
.mockResolvedValue('ok');
|
|
218
|
+
const result = await retryFn(fn);
|
|
219
|
+
expect(result).toBe('ok');
|
|
220
|
+
expect(onRetry).toHaveBeenCalledTimes(1);
|
|
221
|
+
});
|
|
222
|
+
it('allows overriding defaults per call', async () => {
|
|
223
|
+
const defaultOnRetry = vi.fn();
|
|
224
|
+
const callOnRetry = vi.fn();
|
|
225
|
+
const retryFn = createRetryWrapper({
|
|
226
|
+
config: fastConfig,
|
|
227
|
+
shouldRetry: () => true,
|
|
228
|
+
onRetry: defaultOnRetry,
|
|
229
|
+
});
|
|
230
|
+
const fn = vi
|
|
231
|
+
.fn()
|
|
232
|
+
.mockRejectedValueOnce(new Error('fail'))
|
|
233
|
+
.mockResolvedValue('ok');
|
|
234
|
+
await retryFn(fn, { onRetry: callOnRetry });
|
|
235
|
+
// Per-call onRetry should override the default
|
|
236
|
+
expect(callOnRetry).toHaveBeenCalledTimes(1);
|
|
237
|
+
expect(defaultOnRetry).not.toHaveBeenCalled();
|
|
238
|
+
});
|
|
239
|
+
it('merges config from defaults and per-call overrides', async () => {
|
|
240
|
+
const onRetry = vi.fn();
|
|
241
|
+
const retryFn = createRetryWrapper({
|
|
242
|
+
config: { ...fastConfig, maxRetries: 5 },
|
|
243
|
+
shouldRetry: () => true,
|
|
244
|
+
onRetry,
|
|
245
|
+
});
|
|
246
|
+
const fn = vi.fn().mockRejectedValue(new Error('fail'));
|
|
247
|
+
// Override maxRetries to 1 per call
|
|
248
|
+
await expect(retryFn(fn, { config: { maxRetries: 1 } })).rejects.toThrow('fail');
|
|
249
|
+
// 2 calls: attempt 0 + attempt 1
|
|
250
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
// ============================================================================
|
|
254
|
+
// DEFAULT_RETRY_CONFIG
|
|
255
|
+
// ============================================================================
|
|
256
|
+
describe('DEFAULT_RETRY_CONFIG', () => {
|
|
257
|
+
it('has expected default values', () => {
|
|
258
|
+
expect(DEFAULT_RETRY_CONFIG).toEqual({
|
|
259
|
+
maxRetries: 3,
|
|
260
|
+
initialDelayMs: 1000,
|
|
261
|
+
backoffMultiplier: 2,
|
|
262
|
+
maxDelayMs: 10000,
|
|
263
|
+
retryableStatusCodes: [429, 500, 502, 503, 504],
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deployment Status Checker
|
|
3
|
+
*
|
|
4
|
+
* Queries GitHub commit status API for Vercel deployment state.
|
|
5
|
+
* Used to verify deployments before QA/acceptance work can proceed.
|
|
6
|
+
*
|
|
7
|
+
* Self-contained — no dependency on @renseiai/agentfactory core.
|
|
8
|
+
* Originally in packages/core/src/deployment/deployment-checker.ts;
|
|
9
|
+
* duplicated here to keep the linear plugin fully decoupled from core.
|
|
10
|
+
*/
|
|
11
|
+
export interface DeploymentStatus {
|
|
12
|
+
app: string;
|
|
13
|
+
state: 'success' | 'pending' | 'error' | 'failure';
|
|
14
|
+
description: string;
|
|
15
|
+
targetUrl: string | null;
|
|
16
|
+
context: string;
|
|
17
|
+
}
|
|
18
|
+
export interface DeploymentCheckResult {
|
|
19
|
+
allSucceeded: boolean;
|
|
20
|
+
anyFailed: boolean;
|
|
21
|
+
anyPending: boolean;
|
|
22
|
+
statuses: DeploymentStatus[];
|
|
23
|
+
commitSha: string;
|
|
24
|
+
overallState: string;
|
|
25
|
+
}
|
|
26
|
+
interface DeploymentCheckOptions {
|
|
27
|
+
owner?: string;
|
|
28
|
+
repo?: string;
|
|
29
|
+
timeout?: number;
|
|
30
|
+
}
|
|
31
|
+
export declare function checkPRDeploymentStatus(prNumber: number, options?: DeploymentCheckOptions): Promise<DeploymentCheckResult | null>;
|
|
32
|
+
export declare function formatDeploymentStatus(result: DeploymentCheckResult): string;
|
|
33
|
+
export {};
|
|
34
|
+
//# sourceMappingURL=deployment-bridge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deployment-bridge.d.ts","sourceRoot":"","sources":["../../../src/tools/deployment-bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAOH,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS,CAAA;IAClD,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,qBAAqB;IACpC,YAAY,EAAE,OAAO,CAAA;IACrB,SAAS,EAAE,OAAO,CAAA;IAClB,UAAU,EAAE,OAAO,CAAA;IACnB,QAAQ,EAAE,gBAAgB,EAAE,CAAA;IAC5B,SAAS,EAAE,MAAM,CAAA;IACjB,YAAY,EAAE,MAAM,CAAA;CACrB;AAED,UAAU,sBAAsB;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AA2FD,wBAAsB,uBAAuB,CAC3C,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC,CAIvC;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,qBAAqB,GAAG,MAAM,CAmD5E"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deployment Status Checker
|
|
3
|
+
*
|
|
4
|
+
* Queries GitHub commit status API for Vercel deployment state.
|
|
5
|
+
* Used to verify deployments before QA/acceptance work can proceed.
|
|
6
|
+
*
|
|
7
|
+
* Self-contained — no dependency on @renseiai/agentfactory core.
|
|
8
|
+
* Originally in packages/core/src/deployment/deployment-checker.ts;
|
|
9
|
+
* duplicated here to keep the linear plugin fully decoupled from core.
|
|
10
|
+
*/
|
|
11
|
+
import { exec } from 'child_process';
|
|
12
|
+
import { promisify } from 'util';
|
|
13
|
+
const execAsync = promisify(exec);
|
|
14
|
+
const DEFAULT_OPTIONS = {
|
|
15
|
+
owner: 'renseiai',
|
|
16
|
+
repo: 'agentfactory',
|
|
17
|
+
timeout: 30000,
|
|
18
|
+
};
|
|
19
|
+
function parseAppName(context) {
|
|
20
|
+
const match = context.match(/Vercel\s*[–-]\s*(.+)/);
|
|
21
|
+
return match ? match[1].trim() : context;
|
|
22
|
+
}
|
|
23
|
+
function isSuccessfulSkip(description) {
|
|
24
|
+
return description.toLowerCase().includes('skipped') &&
|
|
25
|
+
description.toLowerCase().includes('not affected');
|
|
26
|
+
}
|
|
27
|
+
function normalizeState(state, description) {
|
|
28
|
+
if (isSuccessfulSkip(description))
|
|
29
|
+
return 'success';
|
|
30
|
+
switch (state.toLowerCase()) {
|
|
31
|
+
case 'success': return 'success';
|
|
32
|
+
case 'pending': return 'pending';
|
|
33
|
+
case 'failure': return 'failure';
|
|
34
|
+
case 'error': return 'error';
|
|
35
|
+
default: return 'pending';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function getPRHeadSha(prNumber, options = {}) {
|
|
39
|
+
const { owner, repo, timeout } = { ...DEFAULT_OPTIONS, ...options };
|
|
40
|
+
try {
|
|
41
|
+
const { stdout } = await execAsync(`gh api repos/${owner}/${repo}/pulls/${prNumber} --jq '.head.sha'`, { timeout });
|
|
42
|
+
return stdout.trim() || null;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async function checkDeploymentStatus(commitSha, options = {}) {
|
|
49
|
+
const { owner, repo, timeout } = { ...DEFAULT_OPTIONS, ...options };
|
|
50
|
+
const { stdout } = await execAsync(`gh api repos/${owner}/${repo}/commits/${commitSha}/status`, { timeout });
|
|
51
|
+
const response = JSON.parse(stdout);
|
|
52
|
+
const allStatuses = response.statuses || [];
|
|
53
|
+
const vercelStatuses = allStatuses.filter((s) => s.context.toLowerCase().includes('vercel'));
|
|
54
|
+
const deploymentStatuses = vercelStatuses.map((s) => ({
|
|
55
|
+
app: parseAppName(s.context),
|
|
56
|
+
state: normalizeState(s.state, s.description || ''),
|
|
57
|
+
description: s.description || '',
|
|
58
|
+
targetUrl: s.target_url,
|
|
59
|
+
context: s.context,
|
|
60
|
+
}));
|
|
61
|
+
const allSucceeded = deploymentStatuses.length > 0 &&
|
|
62
|
+
deploymentStatuses.every((s) => s.state === 'success');
|
|
63
|
+
const anyFailed = deploymentStatuses.some((s) => s.state === 'failure' || s.state === 'error');
|
|
64
|
+
const anyPending = deploymentStatuses.some((s) => s.state === 'pending');
|
|
65
|
+
return {
|
|
66
|
+
allSucceeded,
|
|
67
|
+
anyFailed,
|
|
68
|
+
anyPending,
|
|
69
|
+
statuses: deploymentStatuses,
|
|
70
|
+
commitSha,
|
|
71
|
+
overallState: response.state || 'unknown',
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export async function checkPRDeploymentStatus(prNumber, options = {}) {
|
|
75
|
+
const commitSha = await getPRHeadSha(prNumber, options);
|
|
76
|
+
if (!commitSha)
|
|
77
|
+
return null;
|
|
78
|
+
return checkDeploymentStatus(commitSha, options);
|
|
79
|
+
}
|
|
80
|
+
export function formatDeploymentStatus(result) {
|
|
81
|
+
const lines = [];
|
|
82
|
+
lines.push(`## Deployment Status Check`);
|
|
83
|
+
lines.push(``);
|
|
84
|
+
lines.push(`**Commit:** \`${result.commitSha.slice(0, 7)}\``);
|
|
85
|
+
lines.push(`**Overall State:** ${result.overallState}`);
|
|
86
|
+
lines.push(``);
|
|
87
|
+
if (result.statuses.length === 0) {
|
|
88
|
+
lines.push(`No Vercel deployments found for this commit.`);
|
|
89
|
+
return lines.join('\n');
|
|
90
|
+
}
|
|
91
|
+
lines.push(`| App | State | Description |`);
|
|
92
|
+
lines.push(`|-----|-------|-------------|`);
|
|
93
|
+
for (const status of result.statuses) {
|
|
94
|
+
const stateEmoji = {
|
|
95
|
+
success: '\u2705',
|
|
96
|
+
pending: '\u23F3',
|
|
97
|
+
failure: '\u274C',
|
|
98
|
+
error: '\u274C',
|
|
99
|
+
}[status.state];
|
|
100
|
+
const url = status.targetUrl
|
|
101
|
+
? `[${status.app}](${status.targetUrl})`
|
|
102
|
+
: status.app;
|
|
103
|
+
lines.push(`| ${url} | ${stateEmoji} ${status.state} | ${status.description} |`);
|
|
104
|
+
}
|
|
105
|
+
lines.push(``);
|
|
106
|
+
if (result.anyFailed) {
|
|
107
|
+
lines.push(`### \u274C Deployment Failed`);
|
|
108
|
+
lines.push(``);
|
|
109
|
+
lines.push(`One or more Vercel deployments failed. QA/acceptance cannot proceed until deployments succeed.`);
|
|
110
|
+
}
|
|
111
|
+
else if (result.anyPending) {
|
|
112
|
+
lines.push(`### \u23F3 Deployment Pending`);
|
|
113
|
+
lines.push(``);
|
|
114
|
+
lines.push(`One or more Vercel deployments are still in progress.`);
|
|
115
|
+
}
|
|
116
|
+
else if (result.allSucceeded) {
|
|
117
|
+
lines.push(`### \u2705 All Deployments Succeeded`);
|
|
118
|
+
lines.push(``);
|
|
119
|
+
lines.push(`All Vercel deployments have completed successfully.`);
|
|
120
|
+
}
|
|
121
|
+
return lines.join('\n');
|
|
122
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linear Tool Plugin
|
|
3
|
+
*
|
|
4
|
+
* Exposes all Linear CLI commands as typed, in-process agent tools.
|
|
5
|
+
* Agents call these directly instead of shelling out to `pnpm af-linear`.
|
|
6
|
+
*
|
|
7
|
+
* Moved from packages/core/src/tools/plugins/linear.ts to keep
|
|
8
|
+
* Linear-specific tool code in the Linear package.
|
|
9
|
+
*/
|
|
10
|
+
import { type SdkMcpToolDefinition } from '@anthropic-ai/claude-agent-sdk';
|
|
11
|
+
/** A plugin that contributes agent tools from CLI functionality */
|
|
12
|
+
export interface ToolPlugin {
|
|
13
|
+
name: string;
|
|
14
|
+
description: string;
|
|
15
|
+
createTools(context: ToolPluginContext): SdkMcpToolDefinition<any>[];
|
|
16
|
+
}
|
|
17
|
+
/** Context passed to plugins during tool creation */
|
|
18
|
+
export interface ToolPluginContext {
|
|
19
|
+
env: Record<string, string>;
|
|
20
|
+
cwd: string;
|
|
21
|
+
}
|
|
22
|
+
export declare const linearPlugin: ToolPlugin;
|
|
23
|
+
//# sourceMappingURL=linear-plugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"linear-plugin.d.ts","sourceRoot":"","sources":["../../../src/tools/linear-plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,EAAQ,KAAK,oBAAoB,EAAE,MAAM,gCAAgC,CAAA;AAQhF,mEAAmE;AACnE,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,CAAC,OAAO,EAAE,iBAAiB,GAAG,oBAAoB,CAAC,GAAG,CAAC,EAAE,CAAA;CACrE;AAED,qDAAqD;AACrD,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC3B,GAAG,EAAE,MAAM,CAAA;CACZ;AAsOD,eAAO,MAAM,YAAY,EAAE,UAU1B,CAAA"}
|