@renseiai/agentfactory-server 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.
Files changed (93) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +71 -0
  3. package/dist/src/a2a-server.d.ts +88 -0
  4. package/dist/src/a2a-server.d.ts.map +1 -0
  5. package/dist/src/a2a-server.integration.test.d.ts +9 -0
  6. package/dist/src/a2a-server.integration.test.d.ts.map +1 -0
  7. package/dist/src/a2a-server.integration.test.js +397 -0
  8. package/dist/src/a2a-server.js +235 -0
  9. package/dist/src/a2a-server.test.d.ts +2 -0
  10. package/dist/src/a2a-server.test.d.ts.map +1 -0
  11. package/dist/src/a2a-server.test.js +311 -0
  12. package/dist/src/a2a-types.d.ts +125 -0
  13. package/dist/src/a2a-types.d.ts.map +1 -0
  14. package/dist/src/a2a-types.js +8 -0
  15. package/dist/src/agent-tracking.d.ts +201 -0
  16. package/dist/src/agent-tracking.d.ts.map +1 -0
  17. package/dist/src/agent-tracking.js +349 -0
  18. package/dist/src/env-validation.d.ts +65 -0
  19. package/dist/src/env-validation.d.ts.map +1 -0
  20. package/dist/src/env-validation.js +134 -0
  21. package/dist/src/governor-dedup.d.ts +15 -0
  22. package/dist/src/governor-dedup.d.ts.map +1 -0
  23. package/dist/src/governor-dedup.js +31 -0
  24. package/dist/src/governor-event-bus.d.ts +54 -0
  25. package/dist/src/governor-event-bus.d.ts.map +1 -0
  26. package/dist/src/governor-event-bus.js +152 -0
  27. package/dist/src/governor-storage.d.ts +28 -0
  28. package/dist/src/governor-storage.d.ts.map +1 -0
  29. package/dist/src/governor-storage.js +52 -0
  30. package/dist/src/index.d.ts +26 -0
  31. package/dist/src/index.d.ts.map +1 -0
  32. package/dist/src/index.js +50 -0
  33. package/dist/src/issue-lock.d.ts +129 -0
  34. package/dist/src/issue-lock.d.ts.map +1 -0
  35. package/dist/src/issue-lock.js +508 -0
  36. package/dist/src/logger.d.ts +76 -0
  37. package/dist/src/logger.d.ts.map +1 -0
  38. package/dist/src/logger.js +218 -0
  39. package/dist/src/orphan-cleanup.d.ts +64 -0
  40. package/dist/src/orphan-cleanup.d.ts.map +1 -0
  41. package/dist/src/orphan-cleanup.js +369 -0
  42. package/dist/src/pending-prompts.d.ts +67 -0
  43. package/dist/src/pending-prompts.d.ts.map +1 -0
  44. package/dist/src/pending-prompts.js +176 -0
  45. package/dist/src/processing-state-storage.d.ts +38 -0
  46. package/dist/src/processing-state-storage.d.ts.map +1 -0
  47. package/dist/src/processing-state-storage.js +61 -0
  48. package/dist/src/quota-tracker.d.ts +62 -0
  49. package/dist/src/quota-tracker.d.ts.map +1 -0
  50. package/dist/src/quota-tracker.js +155 -0
  51. package/dist/src/rate-limit.d.ts +111 -0
  52. package/dist/src/rate-limit.d.ts.map +1 -0
  53. package/dist/src/rate-limit.js +171 -0
  54. package/dist/src/redis-circuit-breaker.d.ts +67 -0
  55. package/dist/src/redis-circuit-breaker.d.ts.map +1 -0
  56. package/dist/src/redis-circuit-breaker.js +290 -0
  57. package/dist/src/redis-rate-limiter.d.ts +51 -0
  58. package/dist/src/redis-rate-limiter.d.ts.map +1 -0
  59. package/dist/src/redis-rate-limiter.js +168 -0
  60. package/dist/src/redis.d.ts +146 -0
  61. package/dist/src/redis.d.ts.map +1 -0
  62. package/dist/src/redis.js +343 -0
  63. package/dist/src/session-hash.d.ts +48 -0
  64. package/dist/src/session-hash.d.ts.map +1 -0
  65. package/dist/src/session-hash.js +80 -0
  66. package/dist/src/session-storage.d.ts +166 -0
  67. package/dist/src/session-storage.d.ts.map +1 -0
  68. package/dist/src/session-storage.js +397 -0
  69. package/dist/src/token-storage.d.ts +118 -0
  70. package/dist/src/token-storage.d.ts.map +1 -0
  71. package/dist/src/token-storage.js +263 -0
  72. package/dist/src/types.d.ts +11 -0
  73. package/dist/src/types.d.ts.map +1 -0
  74. package/dist/src/types.js +7 -0
  75. package/dist/src/webhook-idempotency.d.ts +44 -0
  76. package/dist/src/webhook-idempotency.d.ts.map +1 -0
  77. package/dist/src/webhook-idempotency.js +148 -0
  78. package/dist/src/work-queue.d.ts +120 -0
  79. package/dist/src/work-queue.d.ts.map +1 -0
  80. package/dist/src/work-queue.js +384 -0
  81. package/dist/src/worker-auth.d.ts +29 -0
  82. package/dist/src/worker-auth.d.ts.map +1 -0
  83. package/dist/src/worker-auth.js +49 -0
  84. package/dist/src/worker-storage.d.ts +108 -0
  85. package/dist/src/worker-storage.d.ts.map +1 -0
  86. package/dist/src/worker-storage.js +295 -0
  87. package/dist/src/workflow-state-integration.test.d.ts +2 -0
  88. package/dist/src/workflow-state-integration.test.d.ts.map +1 -0
  89. package/dist/src/workflow-state-integration.test.js +342 -0
  90. package/dist/src/workflow-state.test.d.ts +2 -0
  91. package/dist/src/workflow-state.test.d.ts.map +1 -0
  92. package/dist/src/workflow-state.test.js +113 -0
  93. package/package.json +72 -0
@@ -0,0 +1,8 @@
1
+ /**
2
+ * A2A Protocol Types
3
+ *
4
+ * TypeScript type definitions for the Agent-to-Agent (A2A) protocol.
5
+ * Covers AgentCard discovery, JSON-RPC messaging, task lifecycle,
6
+ * and Server-Sent Events (SSE) streaming.
7
+ */
8
+ export {};
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Agent Tracking Module
3
+ *
4
+ * Tracks which issues the agent has worked on to enable automated QA pickup.
5
+ * Also tracks QA attempts to prevent infinite loops.
6
+ */
7
+ /**
8
+ * Record of an issue that was worked on by an agent
9
+ */
10
+ export interface AgentWorkRecord {
11
+ issueId: string;
12
+ issueIdentifier: string;
13
+ completedAt: number;
14
+ sessionId: string;
15
+ prUrl?: string;
16
+ }
17
+ /**
18
+ * Record of QA attempts for an issue
19
+ */
20
+ export interface QAAttemptRecord {
21
+ issueId: string;
22
+ attemptNumber: number;
23
+ startedAt: number;
24
+ sessionId: string;
25
+ previousAttempts: Array<{
26
+ sessionId: string;
27
+ failedAt: number;
28
+ reason?: string;
29
+ }>;
30
+ }
31
+ /**
32
+ * Escalation strategy computed deterministically from cycleCount.
33
+ * No LLM in the decision loop — pure function.
34
+ */
35
+ export type EscalationStrategy = 'normal' | 'context-enriched' | 'decompose' | 'escalate-human';
36
+ /**
37
+ * Workflow phase types
38
+ */
39
+ export type WorkflowPhase = 'development' | 'qa' | 'refinement' | 'acceptance';
40
+ /**
41
+ * Record of a single phase attempt within a workflow cycle
42
+ */
43
+ export interface PhaseRecord {
44
+ attempt: number;
45
+ sessionId?: string;
46
+ startedAt: number;
47
+ completedAt?: number;
48
+ result?: 'passed' | 'failed' | 'unknown';
49
+ failureReason?: string;
50
+ costUsd?: number;
51
+ }
52
+ /**
53
+ * Cross-phase workflow state for an issue.
54
+ * Tracks the full dev-QA-rejected cycle count and accumulated failure context.
55
+ * Higher-level overlay on top of the existing QAAttemptRecord.
56
+ */
57
+ export interface WorkflowState {
58
+ issueId: string;
59
+ issueIdentifier: string;
60
+ cycleCount: number;
61
+ phases: {
62
+ development: PhaseRecord[];
63
+ qa: PhaseRecord[];
64
+ refinement: PhaseRecord[];
65
+ acceptance: PhaseRecord[];
66
+ };
67
+ strategy: EscalationStrategy;
68
+ failureSummary: string | null;
69
+ createdAt: number;
70
+ updatedAt: number;
71
+ }
72
+ /**
73
+ * Compute escalation strategy deterministically from cycle count.
74
+ * Pure function — no side effects, no LLM.
75
+ */
76
+ export declare function computeStrategy(cycleCount: number): EscalationStrategy;
77
+ /**
78
+ * Get the workflow state for an issue
79
+ */
80
+ export declare function getWorkflowState(issueId: string): Promise<WorkflowState | null>;
81
+ /**
82
+ * Update (partial merge) the workflow state for an issue.
83
+ * Creates the state if it doesn't exist.
84
+ */
85
+ export declare function updateWorkflowState(issueId: string, partial: Partial<Omit<WorkflowState, 'issueId'>> & {
86
+ issueIdentifier?: string;
87
+ }): Promise<WorkflowState>;
88
+ /**
89
+ * Record a phase attempt in the workflow state
90
+ */
91
+ export declare function recordPhaseAttempt(issueId: string, phase: WorkflowPhase, record: PhaseRecord): Promise<WorkflowState>;
92
+ /**
93
+ * Increment the dev-QA-rejected cycle count and recompute strategy.
94
+ * Called when QA or acceptance fails and triggers a Rejected transition.
95
+ */
96
+ export declare function incrementCycleCount(issueId: string): Promise<WorkflowState>;
97
+ /**
98
+ * Append a failure reason to the accumulated failure summary.
99
+ * Format: --- Cycle {N}, {phase} Attempt {M} ({timestamp}) ---\n{reason}
100
+ */
101
+ export declare function appendFailureSummary(issueId: string, newFailure: string): Promise<WorkflowState>;
102
+ /**
103
+ * Clear workflow state for an issue (called on acceptance pass)
104
+ */
105
+ export declare function clearWorkflowState(issueId: string): Promise<void>;
106
+ /**
107
+ * Extract a structured failure reason from an agent's result message.
108
+ * Strips boilerplate and keeps the substantive failure description.
109
+ */
110
+ export declare function extractFailureReason(resultMessage: string | undefined): string;
111
+ /**
112
+ * Mark an issue as having been worked on by the agent
113
+ */
114
+ export declare function markAgentWorked(issueId: string, data: Omit<AgentWorkRecord, 'issueId' | 'completedAt'>): Promise<void>;
115
+ /**
116
+ * Check if an issue was worked on by the agent
117
+ */
118
+ export declare function wasAgentWorked(issueId: string): Promise<AgentWorkRecord | null>;
119
+ /**
120
+ * Record a QA attempt for an issue
121
+ */
122
+ export declare function recordQAAttempt(issueId: string, sessionId: string): Promise<QAAttemptRecord>;
123
+ /**
124
+ * Get QA attempt count for an issue
125
+ */
126
+ export declare function getQAAttemptCount(issueId: string): Promise<number>;
127
+ /**
128
+ * Mark an issue as having just failed QA (prevents immediate re-trigger)
129
+ */
130
+ export declare function markQAFailed(issueId: string, reason?: string): Promise<void>;
131
+ /**
132
+ * Check if an issue just failed QA (within cooldown period)
133
+ */
134
+ export declare function didJustFailQA(issueId: string): Promise<boolean>;
135
+ /**
136
+ * Clear QA failed marker (when issue is fixed and moves to Finished again)
137
+ */
138
+ export declare function clearQAFailed(issueId: string): Promise<void>;
139
+ /**
140
+ * Clear agent worked record (e.g., when issue is moved back to Backlog)
141
+ */
142
+ export declare function clearAgentWorked(issueId: string): Promise<void>;
143
+ /**
144
+ * Clear QA attempt record (e.g., after successful QA)
145
+ */
146
+ export declare function clearQAAttempts(issueId: string): Promise<void>;
147
+ /**
148
+ * Mark an issue as having development work just queued
149
+ * Prevents rapid re-queuing if status is toggled back and forth
150
+ */
151
+ export declare function markDevelopmentQueued(issueId: string): Promise<void>;
152
+ /**
153
+ * Check if development work was just queued for an issue (within cooldown period)
154
+ */
155
+ export declare function didJustQueueDevelopment(issueId: string): Promise<boolean>;
156
+ /**
157
+ * Clear development queued marker
158
+ */
159
+ export declare function clearDevelopmentQueued(issueId: string): Promise<void>;
160
+ /**
161
+ * Mark an issue as having acceptance work just queued
162
+ * Prevents rapid re-queuing if status is toggled back and forth
163
+ */
164
+ export declare function markAcceptanceQueued(issueId: string): Promise<void>;
165
+ /**
166
+ * Check if acceptance work was just queued for an issue (within cooldown period)
167
+ */
168
+ export declare function didJustQueueAcceptance(issueId: string): Promise<boolean>;
169
+ /**
170
+ * Clear acceptance queued marker
171
+ */
172
+ export declare function clearAcceptanceQueued(issueId: string): Promise<void>;
173
+ /**
174
+ * Clean up all tracking data for an accepted issue
175
+ * Called after successful acceptance processing to remove all Redis state
176
+ */
177
+ export declare function cleanupAcceptedIssue(issueId: string): Promise<void>;
178
+ /**
179
+ * Maximum total sessions (across all phases) allowed per issue.
180
+ * Once reached, no more automated sessions will be created.
181
+ */
182
+ export declare const MAX_TOTAL_SESSIONS = 8;
183
+ /**
184
+ * Get the total number of sessions across all workflow phases for an issue.
185
+ * Returns 0 if no workflow state exists.
186
+ */
187
+ export declare function getTotalSessionCount(issueId: string): Promise<number>;
188
+ /**
189
+ * Mark an issue as having just completed acceptance.
190
+ * Prevents re-triggering acceptance within the cooldown window.
191
+ */
192
+ export declare function markAcceptanceCompleted(issueId: string): Promise<void>;
193
+ /**
194
+ * Check if acceptance was recently completed for an issue (within cooldown period)
195
+ */
196
+ export declare function didAcceptanceJustComplete(issueId: string): Promise<boolean>;
197
+ /**
198
+ * Clear acceptance completed marker
199
+ */
200
+ export declare function clearAcceptanceCompleted(issueId: string): Promise<void>;
201
+ //# sourceMappingURL=agent-tracking.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-tracking.d.ts","sourceRoot":"","sources":["../../src/agent-tracking.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,349 @@
1
+ /**
2
+ * Agent Tracking Module
3
+ *
4
+ * Tracks which issues the agent has worked on to enable automated QA pickup.
5
+ * Also tracks QA attempts to prevent infinite loops.
6
+ */
7
+ import { redisSet, redisGet, redisExists, redisDel } from './redis.js';
8
+ const log = {
9
+ info: (msg, data) => console.log(`[tracking] ${msg}`, data ? JSON.stringify(data) : ''),
10
+ warn: (msg, data) => console.warn(`[tracking] ${msg}`, data ? JSON.stringify(data) : ''),
11
+ error: (msg, data) => console.error(`[tracking] ${msg}`, data ? JSON.stringify(data) : ''),
12
+ debug: (_msg, _data) => { },
13
+ };
14
+ // Redis key prefixes
15
+ const AGENT_WORKED_PREFIX = 'agent:worked:';
16
+ const QA_ATTEMPT_PREFIX = 'qa:attempt:';
17
+ const QA_FAILED_PREFIX = 'qa:failed:';
18
+ const DEV_QUEUED_PREFIX = 'agent:dev-queued:';
19
+ const ACCEPTANCE_QUEUED_PREFIX = 'agent:acceptance-queued:';
20
+ const ACCEPTANCE_COMPLETED_PREFIX = 'agent:acceptance-completed:';
21
+ const WORKFLOW_STATE_PREFIX = 'workflow:state:';
22
+ // TTLs in seconds
23
+ const AGENT_WORKED_TTL = 7 * 24 * 60 * 60; // 7 days
24
+ const QA_ATTEMPT_TTL = 24 * 60 * 60; // 24 hours
25
+ const QA_FAILED_TTL = 60 * 60; // 1 hour
26
+ const DEV_QUEUED_TTL = 300; // 5 minutes
27
+ const ACCEPTANCE_QUEUED_TTL = 300; // 5 minutes
28
+ const ACCEPTANCE_COMPLETED_TTL = 30 * 60; // 30 minutes
29
+ const WORKFLOW_STATE_TTL = 30 * 24 * 60 * 60; // 30 days
30
+ /**
31
+ * Compute escalation strategy deterministically from cycle count.
32
+ * Pure function — no side effects, no LLM.
33
+ */
34
+ export function computeStrategy(cycleCount) {
35
+ if (cycleCount <= 1)
36
+ return 'normal';
37
+ if (cycleCount === 2)
38
+ return 'context-enriched';
39
+ if (cycleCount === 3)
40
+ return 'decompose';
41
+ return 'escalate-human';
42
+ }
43
+ /**
44
+ * Get the workflow state for an issue
45
+ */
46
+ export async function getWorkflowState(issueId) {
47
+ const key = `${WORKFLOW_STATE_PREFIX}${issueId}`;
48
+ return redisGet(key);
49
+ }
50
+ /**
51
+ * Update (partial merge) the workflow state for an issue.
52
+ * Creates the state if it doesn't exist.
53
+ */
54
+ export async function updateWorkflowState(issueId, partial) {
55
+ const key = `${WORKFLOW_STATE_PREFIX}${issueId}`;
56
+ const existing = await redisGet(key);
57
+ const state = {
58
+ issueId,
59
+ issueIdentifier: partial.issueIdentifier ?? existing?.issueIdentifier ?? '',
60
+ cycleCount: partial.cycleCount ?? existing?.cycleCount ?? 0,
61
+ phases: {
62
+ development: partial.phases?.development ?? existing?.phases?.development ?? [],
63
+ qa: partial.phases?.qa ?? existing?.phases?.qa ?? [],
64
+ refinement: partial.phases?.refinement ?? existing?.phases?.refinement ?? [],
65
+ acceptance: partial.phases?.acceptance ?? existing?.phases?.acceptance ?? [],
66
+ },
67
+ strategy: partial.strategy ?? existing?.strategy ?? 'normal',
68
+ failureSummary: partial.failureSummary !== undefined ? partial.failureSummary : (existing?.failureSummary ?? null),
69
+ createdAt: existing?.createdAt ?? Date.now(),
70
+ updatedAt: Date.now(),
71
+ };
72
+ await redisSet(key, state, WORKFLOW_STATE_TTL);
73
+ return state;
74
+ }
75
+ /**
76
+ * Record a phase attempt in the workflow state
77
+ */
78
+ export async function recordPhaseAttempt(issueId, phase, record) {
79
+ const existing = await getWorkflowState(issueId);
80
+ const phases = existing?.phases ?? { development: [], qa: [], refinement: [], acceptance: [] };
81
+ phases[phase] = [...phases[phase], record];
82
+ return updateWorkflowState(issueId, { phases });
83
+ }
84
+ /**
85
+ * Increment the dev-QA-rejected cycle count and recompute strategy.
86
+ * Called when QA or acceptance fails and triggers a Rejected transition.
87
+ */
88
+ export async function incrementCycleCount(issueId) {
89
+ const existing = await getWorkflowState(issueId);
90
+ const newCount = (existing?.cycleCount ?? 0) + 1;
91
+ const strategy = computeStrategy(newCount);
92
+ log.info('Workflow cycle incremented', { issueId, cycleCount: newCount, strategy });
93
+ return updateWorkflowState(issueId, { cycleCount: newCount, strategy });
94
+ }
95
+ /**
96
+ * Append a failure reason to the accumulated failure summary.
97
+ * Format: --- Cycle {N}, {phase} Attempt {M} ({timestamp}) ---\n{reason}
98
+ */
99
+ export async function appendFailureSummary(issueId, newFailure) {
100
+ const existing = await getWorkflowState(issueId);
101
+ const currentSummary = existing?.failureSummary ?? '';
102
+ const updatedSummary = currentSummary
103
+ ? `${currentSummary}\n\n${newFailure}`
104
+ : newFailure;
105
+ return updateWorkflowState(issueId, { failureSummary: updatedSummary });
106
+ }
107
+ /**
108
+ * Clear workflow state for an issue (called on acceptance pass)
109
+ */
110
+ export async function clearWorkflowState(issueId) {
111
+ const key = `${WORKFLOW_STATE_PREFIX}${issueId}`;
112
+ await redisDel(key);
113
+ log.info('Cleared workflow state', { issueId });
114
+ }
115
+ /**
116
+ * Extract a structured failure reason from an agent's result message.
117
+ * Strips boilerplate and keeps the substantive failure description.
118
+ */
119
+ export function extractFailureReason(resultMessage) {
120
+ if (!resultMessage)
121
+ return 'No result message provided';
122
+ // Try to extract from structured sections
123
+ const failurePatterns = [
124
+ /##\s*(?:QA\s+)?(?:Failed|Failure|Issues?\s+Found)[^\n]*\n([\s\S]*?)(?=\n##|\n<!--|\Z)/i,
125
+ /(?:Fail(?:ure|ed)\s+(?:Reason|Details?|Summary)):?\s*([\s\S]*?)(?=\n##|\n<!--|\Z)/i,
126
+ /(?:Issues?\s+Found|Problems?\s+Found):?\s*([\s\S]*?)(?=\n##|\n<!--|\Z)/i,
127
+ ];
128
+ for (const pattern of failurePatterns) {
129
+ const match = resultMessage.match(pattern);
130
+ if (match?.[1]) {
131
+ const extracted = match[1].trim();
132
+ if (extracted.length > 20) {
133
+ // Truncate if excessively long
134
+ return extracted.length > 2000 ? extracted.slice(0, 2000) + '...' : extracted;
135
+ }
136
+ }
137
+ }
138
+ // Fallback: take the last meaningful paragraph (often the conclusion)
139
+ const paragraphs = resultMessage.split(/\n\n+/).filter(p => p.trim().length > 20);
140
+ if (paragraphs.length > 0) {
141
+ const last = paragraphs[paragraphs.length - 1].trim();
142
+ return last.length > 2000 ? last.slice(0, 2000) + '...' : last;
143
+ }
144
+ return resultMessage.slice(0, 500);
145
+ }
146
+ /**
147
+ * Mark an issue as having been worked on by the agent
148
+ */
149
+ export async function markAgentWorked(issueId, data) {
150
+ const key = `${AGENT_WORKED_PREFIX}${issueId}`;
151
+ const record = {
152
+ ...data,
153
+ issueId,
154
+ completedAt: Date.now(),
155
+ };
156
+ await redisSet(key, record, AGENT_WORKED_TTL);
157
+ log.info('Marked agent worked', {
158
+ issueId,
159
+ issueIdentifier: data.issueIdentifier,
160
+ });
161
+ }
162
+ /**
163
+ * Check if an issue was worked on by the agent
164
+ */
165
+ export async function wasAgentWorked(issueId) {
166
+ const key = `${AGENT_WORKED_PREFIX}${issueId}`;
167
+ return redisGet(key);
168
+ }
169
+ /**
170
+ * Record a QA attempt for an issue
171
+ */
172
+ export async function recordQAAttempt(issueId, sessionId) {
173
+ const key = `${QA_ATTEMPT_PREFIX}${issueId}`;
174
+ const existing = await redisGet(key);
175
+ const record = {
176
+ issueId,
177
+ attemptNumber: (existing?.attemptNumber ?? 0) + 1,
178
+ startedAt: Date.now(),
179
+ sessionId,
180
+ previousAttempts: existing?.previousAttempts ?? [],
181
+ };
182
+ // Add current attempt to history if this is a retry
183
+ if (existing) {
184
+ record.previousAttempts.push({
185
+ sessionId: existing.sessionId,
186
+ failedAt: Date.now(),
187
+ });
188
+ }
189
+ await redisSet(key, record, QA_ATTEMPT_TTL);
190
+ log.info('Recorded QA attempt', {
191
+ issueId,
192
+ attemptNumber: record.attemptNumber,
193
+ });
194
+ return record;
195
+ }
196
+ /**
197
+ * Get QA attempt count for an issue
198
+ */
199
+ export async function getQAAttemptCount(issueId) {
200
+ const key = `${QA_ATTEMPT_PREFIX}${issueId}`;
201
+ const record = await redisGet(key);
202
+ return record?.attemptNumber ?? 0;
203
+ }
204
+ /**
205
+ * Mark an issue as having just failed QA (prevents immediate re-trigger)
206
+ */
207
+ export async function markQAFailed(issueId, reason) {
208
+ const key = `${QA_FAILED_PREFIX}${issueId}`;
209
+ await redisSet(key, { failedAt: Date.now(), reason }, QA_FAILED_TTL);
210
+ log.info('Marked QA failed', { issueId, reason });
211
+ }
212
+ /**
213
+ * Check if an issue just failed QA (within cooldown period)
214
+ */
215
+ export async function didJustFailQA(issueId) {
216
+ const key = `${QA_FAILED_PREFIX}${issueId}`;
217
+ return redisExists(key);
218
+ }
219
+ /**
220
+ * Clear QA failed marker (when issue is fixed and moves to Finished again)
221
+ */
222
+ export async function clearQAFailed(issueId) {
223
+ const key = `${QA_FAILED_PREFIX}${issueId}`;
224
+ await redisDel(key);
225
+ log.debug('Cleared QA failed marker', { issueId });
226
+ }
227
+ /**
228
+ * Clear agent worked record (e.g., when issue is moved back to Backlog)
229
+ */
230
+ export async function clearAgentWorked(issueId) {
231
+ const key = `${AGENT_WORKED_PREFIX}${issueId}`;
232
+ await redisDel(key);
233
+ log.debug('Cleared agent worked marker', { issueId });
234
+ }
235
+ /**
236
+ * Clear QA attempt record (e.g., after successful QA)
237
+ */
238
+ export async function clearQAAttempts(issueId) {
239
+ const key = `${QA_ATTEMPT_PREFIX}${issueId}`;
240
+ await redisDel(key);
241
+ log.debug('Cleared QA attempts', { issueId });
242
+ }
243
+ /**
244
+ * Mark an issue as having development work just queued
245
+ * Prevents rapid re-queuing if status is toggled back and forth
246
+ */
247
+ export async function markDevelopmentQueued(issueId) {
248
+ const key = `${DEV_QUEUED_PREFIX}${issueId}`;
249
+ await redisSet(key, { queuedAt: Date.now() }, DEV_QUEUED_TTL);
250
+ log.info('Marked development queued', { issueId });
251
+ }
252
+ /**
253
+ * Check if development work was just queued for an issue (within cooldown period)
254
+ */
255
+ export async function didJustQueueDevelopment(issueId) {
256
+ const key = `${DEV_QUEUED_PREFIX}${issueId}`;
257
+ return redisExists(key);
258
+ }
259
+ /**
260
+ * Clear development queued marker
261
+ */
262
+ export async function clearDevelopmentQueued(issueId) {
263
+ const key = `${DEV_QUEUED_PREFIX}${issueId}`;
264
+ await redisDel(key);
265
+ log.debug('Cleared development queued marker', { issueId });
266
+ }
267
+ /**
268
+ * Mark an issue as having acceptance work just queued
269
+ * Prevents rapid re-queuing if status is toggled back and forth
270
+ */
271
+ export async function markAcceptanceQueued(issueId) {
272
+ const key = `${ACCEPTANCE_QUEUED_PREFIX}${issueId}`;
273
+ await redisSet(key, { queuedAt: Date.now() }, ACCEPTANCE_QUEUED_TTL);
274
+ log.info('Marked acceptance queued', { issueId });
275
+ }
276
+ /**
277
+ * Check if acceptance work was just queued for an issue (within cooldown period)
278
+ */
279
+ export async function didJustQueueAcceptance(issueId) {
280
+ const key = `${ACCEPTANCE_QUEUED_PREFIX}${issueId}`;
281
+ return redisExists(key);
282
+ }
283
+ /**
284
+ * Clear acceptance queued marker
285
+ */
286
+ export async function clearAcceptanceQueued(issueId) {
287
+ const key = `${ACCEPTANCE_QUEUED_PREFIX}${issueId}`;
288
+ await redisDel(key);
289
+ log.debug('Cleared acceptance queued marker', { issueId });
290
+ }
291
+ /**
292
+ * Clean up all tracking data for an accepted issue
293
+ * Called after successful acceptance processing to remove all Redis state
294
+ */
295
+ export async function cleanupAcceptedIssue(issueId) {
296
+ const keysToDelete = [
297
+ `${AGENT_WORKED_PREFIX}${issueId}`,
298
+ `${QA_ATTEMPT_PREFIX}${issueId}`,
299
+ `${QA_FAILED_PREFIX}${issueId}`,
300
+ `${DEV_QUEUED_PREFIX}${issueId}`,
301
+ `${ACCEPTANCE_QUEUED_PREFIX}${issueId}`,
302
+ `${ACCEPTANCE_COMPLETED_PREFIX}${issueId}`,
303
+ `${WORKFLOW_STATE_PREFIX}${issueId}`,
304
+ ];
305
+ await Promise.all(keysToDelete.map((key) => redisDel(key)));
306
+ log.info('Cleaned up all tracking data for accepted issue', { issueId });
307
+ }
308
+ /**
309
+ * Maximum total sessions (across all phases) allowed per issue.
310
+ * Once reached, no more automated sessions will be created.
311
+ */
312
+ export const MAX_TOTAL_SESSIONS = 8;
313
+ /**
314
+ * Get the total number of sessions across all workflow phases for an issue.
315
+ * Returns 0 if no workflow state exists.
316
+ */
317
+ export async function getTotalSessionCount(issueId) {
318
+ const state = await getWorkflowState(issueId);
319
+ if (!state)
320
+ return 0;
321
+ return (state.phases.development.length +
322
+ state.phases.qa.length +
323
+ state.phases.refinement.length +
324
+ state.phases.acceptance.length);
325
+ }
326
+ /**
327
+ * Mark an issue as having just completed acceptance.
328
+ * Prevents re-triggering acceptance within the cooldown window.
329
+ */
330
+ export async function markAcceptanceCompleted(issueId) {
331
+ const key = `${ACCEPTANCE_COMPLETED_PREFIX}${issueId}`;
332
+ await redisSet(key, { completedAt: Date.now() }, ACCEPTANCE_COMPLETED_TTL);
333
+ log.info('Marked acceptance completed', { issueId });
334
+ }
335
+ /**
336
+ * Check if acceptance was recently completed for an issue (within cooldown period)
337
+ */
338
+ export async function didAcceptanceJustComplete(issueId) {
339
+ const key = `${ACCEPTANCE_COMPLETED_PREFIX}${issueId}`;
340
+ return redisExists(key);
341
+ }
342
+ /**
343
+ * Clear acceptance completed marker
344
+ */
345
+ export async function clearAcceptanceCompleted(issueId) {
346
+ const key = `${ACCEPTANCE_COMPLETED_PREFIX}${issueId}`;
347
+ await redisDel(key);
348
+ log.debug('Cleared acceptance completed marker', { issueId });
349
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Environment Variable Validation
3
+ *
4
+ * Validates required environment variables on startup.
5
+ * Fails fast if critical security variables are missing in production.
6
+ */
7
+ /**
8
+ * Configuration for environment validation
9
+ */
10
+ export interface EnvValidationConfig {
11
+ /** Variable names required in production */
12
+ requiredVars?: string[];
13
+ /** Variables that need minimum length validation */
14
+ minLengthVars?: Array<{
15
+ name: string;
16
+ minLength: number;
17
+ }>;
18
+ }
19
+ /**
20
+ * Validation result
21
+ */
22
+ export interface EnvValidationResult {
23
+ valid: boolean;
24
+ missing: string[];
25
+ warnings: string[];
26
+ }
27
+ /**
28
+ * Validate environment variables
29
+ *
30
+ * In production: All required vars must be present
31
+ * In development: Log warnings for missing vars but don't fail
32
+ *
33
+ * @param config - Optional configuration to override defaults
34
+ * @returns Validation result with missing vars
35
+ */
36
+ export declare function validateEnv(config?: EnvValidationConfig): EnvValidationResult;
37
+ /**
38
+ * Validate and fail fast if critical vars are missing
39
+ *
40
+ * Call this at application startup to ensure required
41
+ * environment variables are configured.
42
+ *
43
+ * @param config - Optional configuration to override defaults
44
+ * @throws Error if required vars are missing in production
45
+ */
46
+ export declare function validateEnvOrThrow(config?: EnvValidationConfig): void;
47
+ /**
48
+ * Check if webhook signature verification is configured
49
+ */
50
+ export declare function isWebhookSecretConfigured(): boolean;
51
+ /**
52
+ * Check if cron authentication is configured
53
+ */
54
+ export declare function isCronSecretConfigured(): boolean;
55
+ /**
56
+ * Check if session hashing is configured
57
+ */
58
+ export declare function isSessionHashConfigured(): boolean;
59
+ /**
60
+ * Get session hash salt
61
+ * @param saltEnvVar - Environment variable name for the salt (default: SESSION_HASH_SALT)
62
+ * @throws Error if not configured
63
+ */
64
+ export declare function getSessionHashSalt(saltEnvVar?: string): string;
65
+ //# sourceMappingURL=env-validation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env-validation.d.ts","sourceRoot":"","sources":["../../src/env-validation.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,4CAA4C;IAC5C,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;IACvB,oDAAoD;IACpD,aAAa,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAC3D;AA8BD;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,OAAO,CAAA;IACd,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAA;CACnB;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,MAAM,CAAC,EAAE,mBAAmB,GAAG,mBAAmB,CAkC7E;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,CAAC,EAAE,mBAAmB,GAAG,IAAI,CAsBrE;AAED;;GAEG;AACH,wBAAgB,yBAAyB,IAAI,OAAO,CAEnD;AAED;;GAEG;AACH,wBAAgB,sBAAsB,IAAI,OAAO,CAEhD;AAED;;GAEG;AACH,wBAAgB,uBAAuB,IAAI,OAAO,CAGjD;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,SAAsB,GAAG,MAAM,CAM3E"}