@renseiai/agentfactory-cli 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 +123 -0
- package/dist/src/agent.d.ts +20 -0
- package/dist/src/agent.d.ts.map +1 -0
- package/dist/src/agent.js +109 -0
- package/dist/src/analyze-logs.d.ts +26 -0
- package/dist/src/analyze-logs.d.ts.map +1 -0
- package/dist/src/analyze-logs.js +152 -0
- package/dist/src/cleanup.d.ts +17 -0
- package/dist/src/cleanup.d.ts.map +1 -0
- package/dist/src/cleanup.js +111 -0
- package/dist/src/governor.d.ts +26 -0
- package/dist/src/governor.d.ts.map +1 -0
- package/dist/src/governor.js +305 -0
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +76 -0
- package/dist/src/lib/agent-runner.d.ts +28 -0
- package/dist/src/lib/agent-runner.d.ts.map +1 -0
- package/dist/src/lib/agent-runner.js +272 -0
- package/dist/src/lib/analyze-logs-runner.d.ts +47 -0
- package/dist/src/lib/analyze-logs-runner.d.ts.map +1 -0
- package/dist/src/lib/analyze-logs-runner.js +216 -0
- package/dist/src/lib/auto-updater.d.ts +40 -0
- package/dist/src/lib/auto-updater.d.ts.map +1 -0
- package/dist/src/lib/auto-updater.js +109 -0
- package/dist/src/lib/cleanup-runner.d.ts +29 -0
- package/dist/src/lib/cleanup-runner.d.ts.map +1 -0
- package/dist/src/lib/cleanup-runner.js +295 -0
- package/dist/src/lib/governor-dependencies.d.ts +23 -0
- package/dist/src/lib/governor-dependencies.d.ts.map +1 -0
- package/dist/src/lib/governor-dependencies.js +361 -0
- package/dist/src/lib/governor-logger.d.ts +30 -0
- package/dist/src/lib/governor-logger.d.ts.map +1 -0
- package/dist/src/lib/governor-logger.js +210 -0
- package/dist/src/lib/governor-runner.d.ts +103 -0
- package/dist/src/lib/governor-runner.d.ts.map +1 -0
- package/dist/src/lib/governor-runner.js +210 -0
- package/dist/src/lib/linear-runner.d.ts +8 -0
- package/dist/src/lib/linear-runner.d.ts.map +1 -0
- package/dist/src/lib/linear-runner.js +7 -0
- package/dist/src/lib/orchestrator-runner.d.ts +51 -0
- package/dist/src/lib/orchestrator-runner.d.ts.map +1 -0
- package/dist/src/lib/orchestrator-runner.js +151 -0
- package/dist/src/lib/queue-admin-runner.d.ts +30 -0
- package/dist/src/lib/queue-admin-runner.d.ts.map +1 -0
- package/dist/src/lib/queue-admin-runner.js +378 -0
- package/dist/src/lib/sync-routes-runner.d.ts +28 -0
- package/dist/src/lib/sync-routes-runner.d.ts.map +1 -0
- package/dist/src/lib/sync-routes-runner.js +110 -0
- package/dist/src/lib/version.d.ts +35 -0
- package/dist/src/lib/version.d.ts.map +1 -0
- package/dist/src/lib/version.js +168 -0
- package/dist/src/lib/worker-fleet-runner.d.ts +32 -0
- package/dist/src/lib/worker-fleet-runner.d.ts.map +1 -0
- package/dist/src/lib/worker-fleet-runner.js +256 -0
- package/dist/src/lib/worker-runner.d.ts +33 -0
- package/dist/src/lib/worker-runner.d.ts.map +1 -0
- package/dist/src/lib/worker-runner.js +781 -0
- package/dist/src/linear.d.ts +37 -0
- package/dist/src/linear.d.ts.map +1 -0
- package/dist/src/linear.js +118 -0
- package/dist/src/orchestrator.d.ts +21 -0
- package/dist/src/orchestrator.d.ts.map +1 -0
- package/dist/src/orchestrator.js +190 -0
- package/dist/src/queue-admin.d.ts +25 -0
- package/dist/src/queue-admin.d.ts.map +1 -0
- package/dist/src/queue-admin.js +96 -0
- package/dist/src/sync-routes.d.ts +17 -0
- package/dist/src/sync-routes.d.ts.map +1 -0
- package/dist/src/sync-routes.js +100 -0
- package/dist/src/worker-fleet.d.ts +25 -0
- package/dist/src/worker-fleet.d.ts.map +1 -0
- package/dist/src/worker-fleet.js +140 -0
- package/dist/src/worker.d.ts +26 -0
- package/dist/src/worker.d.ts.map +1 -0
- package/dist/src/worker.js +135 -0
- package/package.json +175 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Real Governor Dependencies
|
|
3
|
+
*
|
|
4
|
+
* Maps each GovernorDependencies callback to its real implementation
|
|
5
|
+
* using the Linear SDK (via LinearAgentClient) and Redis storage
|
|
6
|
+
* (from @renseiai/agentfactory-server).
|
|
7
|
+
*/
|
|
8
|
+
import { isHeld as checkIsHeld, getOverridePriority as checkOverridePriority, } from '@renseiai/agentfactory';
|
|
9
|
+
import { getSessionStateByIssue, didJustFailQA, getWorkflowState, getTotalSessionCount, RedisProcessingStateStorage, storeSessionState, dispatchWork as issueLockDispatchWork, } from '@renseiai/agentfactory-server';
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Logging
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
const log = {
|
|
14
|
+
info: (msg, data) => console.log(`[governor-deps] ${msg}`, data ? JSON.stringify(data) : ''),
|
|
15
|
+
warn: (msg, data) => console.warn(`[governor-deps] ${msg}`, data ? JSON.stringify(data) : ''),
|
|
16
|
+
error: (msg, data) => console.error(`[governor-deps] ${msg}`, data ? JSON.stringify(data) : ''),
|
|
17
|
+
};
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Action-to-WorkType mapping
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
function actionToWorkType(action) {
|
|
22
|
+
switch (action) {
|
|
23
|
+
case 'trigger-research':
|
|
24
|
+
return 'research';
|
|
25
|
+
case 'trigger-backlog-creation':
|
|
26
|
+
return 'backlog-creation';
|
|
27
|
+
case 'trigger-development':
|
|
28
|
+
return 'development';
|
|
29
|
+
case 'trigger-qa':
|
|
30
|
+
return 'qa';
|
|
31
|
+
case 'trigger-acceptance':
|
|
32
|
+
return 'acceptance';
|
|
33
|
+
case 'trigger-refinement':
|
|
34
|
+
return 'refinement';
|
|
35
|
+
case 'decompose':
|
|
36
|
+
return 'coordination';
|
|
37
|
+
case 'escalate-human':
|
|
38
|
+
return 'escalation';
|
|
39
|
+
default:
|
|
40
|
+
return 'development';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Create real GovernorDependencies backed by the Linear SDK and Redis.
|
|
45
|
+
*
|
|
46
|
+
* Each callback wraps its implementation in a try/catch so that a single
|
|
47
|
+
* failing dependency does not crash the entire governor scan loop.
|
|
48
|
+
*/
|
|
49
|
+
export function createRealDependencies(config) {
|
|
50
|
+
const processingState = new RedisProcessingStateStorage();
|
|
51
|
+
// Caches populated by listIssues() single GraphQL query.
|
|
52
|
+
// parentIssueIds: issues with children (isParent = true)
|
|
53
|
+
// scannedIssueIds: all issues returned by the last scan (isParent known definitively)
|
|
54
|
+
// Only issues NOT in scannedIssueIds need an API fallback (e.g., webhook-driven).
|
|
55
|
+
const parentIssueIds = new Set();
|
|
56
|
+
const scannedIssueIds = new Set();
|
|
57
|
+
return {
|
|
58
|
+
// -----------------------------------------------------------------------
|
|
59
|
+
// 1. listIssues -- scan Linear project using single GraphQL query
|
|
60
|
+
// -----------------------------------------------------------------------
|
|
61
|
+
listIssues: async (project) => {
|
|
62
|
+
try {
|
|
63
|
+
const rawIssues = await config.linearClient.listProjectIssues(project);
|
|
64
|
+
// Cache issue IDs for isParentIssue() lookups — avoids per-issue API calls
|
|
65
|
+
parentIssueIds.clear();
|
|
66
|
+
scannedIssueIds.clear();
|
|
67
|
+
for (const issue of rawIssues) {
|
|
68
|
+
scannedIssueIds.add(issue.id);
|
|
69
|
+
if (issue.childCount > 0) {
|
|
70
|
+
parentIssueIds.add(issue.id);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return rawIssues.map((issue) => ({
|
|
74
|
+
id: issue.id,
|
|
75
|
+
identifier: issue.identifier,
|
|
76
|
+
title: issue.title,
|
|
77
|
+
description: issue.description,
|
|
78
|
+
status: issue.status,
|
|
79
|
+
labels: issue.labels,
|
|
80
|
+
createdAt: issue.createdAt,
|
|
81
|
+
parentId: issue.parentId,
|
|
82
|
+
project: issue.project,
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
log.error('listIssues failed', {
|
|
87
|
+
project,
|
|
88
|
+
error: err instanceof Error ? err.message : String(err),
|
|
89
|
+
});
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
// -----------------------------------------------------------------------
|
|
94
|
+
// 2. hasActiveSession -- check Redis session storage
|
|
95
|
+
// -----------------------------------------------------------------------
|
|
96
|
+
hasActiveSession: async (issueId) => {
|
|
97
|
+
try {
|
|
98
|
+
const session = await getSessionStateByIssue(issueId);
|
|
99
|
+
if (!session)
|
|
100
|
+
return false;
|
|
101
|
+
const activeStatuses = ['running', 'claimed', 'pending', 'finalizing'];
|
|
102
|
+
return activeStatuses.includes(session.status);
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
log.error('hasActiveSession failed', {
|
|
106
|
+
issueId,
|
|
107
|
+
error: err instanceof Error ? err.message : String(err),
|
|
108
|
+
});
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
// -----------------------------------------------------------------------
|
|
113
|
+
// 3. isWithinCooldown -- check if QA just failed for this issue
|
|
114
|
+
// -----------------------------------------------------------------------
|
|
115
|
+
isWithinCooldown: async (issueId) => {
|
|
116
|
+
try {
|
|
117
|
+
return await didJustFailQA(issueId);
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
log.error('isWithinCooldown failed', {
|
|
121
|
+
issueId,
|
|
122
|
+
error: err instanceof Error ? err.message : String(err),
|
|
123
|
+
});
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
// -----------------------------------------------------------------------
|
|
128
|
+
// 4. isParentIssue -- check cache first, fall back to API
|
|
129
|
+
// -----------------------------------------------------------------------
|
|
130
|
+
isParentIssue: async (issueId) => {
|
|
131
|
+
// Check cached parent IDs from listIssues (populated by single GraphQL query)
|
|
132
|
+
if (parentIssueIds.has(issueId))
|
|
133
|
+
return true;
|
|
134
|
+
// If the issue was in the scan, we know definitively it's not a parent
|
|
135
|
+
if (scannedIssueIds.has(issueId))
|
|
136
|
+
return false;
|
|
137
|
+
// Fall back to API only for issues not in the last scan (e.g., webhook-driven)
|
|
138
|
+
try {
|
|
139
|
+
return await config.linearClient.isParentIssue(issueId);
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
log.error('isParentIssue failed', {
|
|
143
|
+
issueId,
|
|
144
|
+
error: err instanceof Error ? err.message : String(err),
|
|
145
|
+
});
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
// -----------------------------------------------------------------------
|
|
150
|
+
// 5. isHeld -- check touchpoint override storage
|
|
151
|
+
// -----------------------------------------------------------------------
|
|
152
|
+
isHeld: async (issueId) => {
|
|
153
|
+
try {
|
|
154
|
+
return await checkIsHeld(issueId);
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
log.error('isHeld failed', {
|
|
158
|
+
issueId,
|
|
159
|
+
error: err instanceof Error ? err.message : String(err),
|
|
160
|
+
});
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
// -----------------------------------------------------------------------
|
|
165
|
+
// 6. getOverridePriority -- check touchpoint override storage
|
|
166
|
+
// -----------------------------------------------------------------------
|
|
167
|
+
getOverridePriority: async (issueId) => {
|
|
168
|
+
try {
|
|
169
|
+
return await checkOverridePriority(issueId);
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
log.error('getOverridePriority failed', {
|
|
173
|
+
issueId,
|
|
174
|
+
error: err instanceof Error ? err.message : String(err),
|
|
175
|
+
});
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
// -----------------------------------------------------------------------
|
|
180
|
+
// 7. getWorkflowStrategy -- check Redis workflow state
|
|
181
|
+
// -----------------------------------------------------------------------
|
|
182
|
+
getWorkflowStrategy: async (issueId) => {
|
|
183
|
+
try {
|
|
184
|
+
const workflowState = await getWorkflowState(issueId);
|
|
185
|
+
return workflowState?.strategy;
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
log.error('getWorkflowStrategy failed', {
|
|
189
|
+
issueId,
|
|
190
|
+
error: err instanceof Error ? err.message : String(err),
|
|
191
|
+
});
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
// -----------------------------------------------------------------------
|
|
196
|
+
// 8. isResearchCompleted -- check Redis processing state
|
|
197
|
+
// -----------------------------------------------------------------------
|
|
198
|
+
isResearchCompleted: async (issueId) => {
|
|
199
|
+
try {
|
|
200
|
+
return await processingState.isPhaseCompleted(issueId, 'research');
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
log.error('isResearchCompleted failed', {
|
|
204
|
+
issueId,
|
|
205
|
+
error: err instanceof Error ? err.message : String(err),
|
|
206
|
+
});
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
// -----------------------------------------------------------------------
|
|
211
|
+
// 9. isBacklogCreationCompleted -- check Redis processing state
|
|
212
|
+
// -----------------------------------------------------------------------
|
|
213
|
+
isBacklogCreationCompleted: async (issueId) => {
|
|
214
|
+
try {
|
|
215
|
+
return await processingState.isPhaseCompleted(issueId, 'backlog-creation');
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
log.error('isBacklogCreationCompleted failed', {
|
|
219
|
+
issueId,
|
|
220
|
+
error: err instanceof Error ? err.message : String(err),
|
|
221
|
+
});
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
// -----------------------------------------------------------------------
|
|
226
|
+
// 10. getCompletedSessionCount -- count completed sessions for circuit breaker
|
|
227
|
+
// -----------------------------------------------------------------------
|
|
228
|
+
getCompletedSessionCount: async (issueId) => {
|
|
229
|
+
try {
|
|
230
|
+
return await getTotalSessionCount(issueId);
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
log.error('getCompletedSessionCount failed', {
|
|
234
|
+
issueId,
|
|
235
|
+
error: err instanceof Error ? err.message : String(err),
|
|
236
|
+
});
|
|
237
|
+
return 0;
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
// -----------------------------------------------------------------------
|
|
241
|
+
// 11. dispatchWork -- create Linear session and queue work
|
|
242
|
+
// Accepts GovernorIssue directly (already resolved in the scan),
|
|
243
|
+
// eliminating 2 redundant API calls per dispatch.
|
|
244
|
+
// -----------------------------------------------------------------------
|
|
245
|
+
dispatchWork: async (issue, action) => {
|
|
246
|
+
const issueId = issue.id;
|
|
247
|
+
const issueIdentifier = issue.identifier;
|
|
248
|
+
const projectName = issue.project;
|
|
249
|
+
try {
|
|
250
|
+
let workType = actionToWorkType(action);
|
|
251
|
+
// Parent issues use coordination variants for development, QA, acceptance, and refinement
|
|
252
|
+
if (parentIssueIds.has(issueId)) {
|
|
253
|
+
if (workType === 'development')
|
|
254
|
+
workType = 'coordination';
|
|
255
|
+
else if (workType === 'qa')
|
|
256
|
+
workType = 'qa-coordination';
|
|
257
|
+
else if (workType === 'acceptance')
|
|
258
|
+
workType = 'acceptance-coordination';
|
|
259
|
+
else if (workType === 'refinement')
|
|
260
|
+
workType = 'refinement-coordination';
|
|
261
|
+
}
|
|
262
|
+
log.info('Dispatching work', { issueId, issueIdentifier, action, workType });
|
|
263
|
+
// Create a Linear Agent Session on the issue so the UI shows activity
|
|
264
|
+
// Resolve OAuth client fresh each dispatch (handles token refresh/expiry)
|
|
265
|
+
const oauthClient = await config.resolveOAuthClient?.();
|
|
266
|
+
const sessionClient = oauthClient ?? config.linearClient;
|
|
267
|
+
let sessionId;
|
|
268
|
+
try {
|
|
269
|
+
const sessionResult = await sessionClient.createAgentSessionOnIssue({
|
|
270
|
+
issueId,
|
|
271
|
+
});
|
|
272
|
+
sessionId = sessionResult.sessionId;
|
|
273
|
+
}
|
|
274
|
+
catch (err) {
|
|
275
|
+
log.warn('Could not create agent session, will queue without sessionId', {
|
|
276
|
+
issueId,
|
|
277
|
+
error: err instanceof Error ? err.message : String(err),
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
const finalSessionId = sessionId ?? `governor-${issueId}-${Date.now()}`;
|
|
281
|
+
const now = Date.now();
|
|
282
|
+
// Fetch workflow state for retry context injection
|
|
283
|
+
const workflowState = await getWorkflowState(issueId);
|
|
284
|
+
const workflowContext = workflowState?.cycleCount
|
|
285
|
+
? {
|
|
286
|
+
cycleCount: workflowState.cycleCount,
|
|
287
|
+
strategy: workflowState.strategy,
|
|
288
|
+
failureSummary: workflowState.failureSummary,
|
|
289
|
+
}
|
|
290
|
+
: undefined;
|
|
291
|
+
// Generate prompt for the work type (with retry context if available)
|
|
292
|
+
const prompt = config.generatePrompt?.(issueIdentifier, workType, undefined, workflowContext);
|
|
293
|
+
// Register a pending session FIRST so hasActiveSession() returns true
|
|
294
|
+
// immediately, preventing re-dispatch on subsequent poll sweeps.
|
|
295
|
+
await storeSessionState(finalSessionId, {
|
|
296
|
+
issueId,
|
|
297
|
+
issueIdentifier,
|
|
298
|
+
providerSessionId: null,
|
|
299
|
+
worktreePath: '',
|
|
300
|
+
status: 'pending',
|
|
301
|
+
workerId: null,
|
|
302
|
+
queuedAt: now,
|
|
303
|
+
priority: 3,
|
|
304
|
+
workType: workType,
|
|
305
|
+
projectName,
|
|
306
|
+
organizationId: config.organizationId,
|
|
307
|
+
promptContext: prompt,
|
|
308
|
+
});
|
|
309
|
+
// Queue the work item for a worker to pick up
|
|
310
|
+
const queuedWork = {
|
|
311
|
+
sessionId: finalSessionId,
|
|
312
|
+
issueId,
|
|
313
|
+
issueIdentifier,
|
|
314
|
+
priority: 3,
|
|
315
|
+
queuedAt: now,
|
|
316
|
+
workType: workType,
|
|
317
|
+
projectName,
|
|
318
|
+
prompt,
|
|
319
|
+
};
|
|
320
|
+
// Use issue-lock dispatch instead of raw queueWork().
|
|
321
|
+
// If the issue is already locked (another session is in-flight),
|
|
322
|
+
// work is parked and promoted when the lock is released.
|
|
323
|
+
const result = await issueLockDispatchWork(queuedWork);
|
|
324
|
+
if (!result.dispatched && !result.parked) {
|
|
325
|
+
log.warn('Failed to dispatch or park work', {
|
|
326
|
+
issueId,
|
|
327
|
+
action,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
else if (result.parked) {
|
|
331
|
+
log.info('Work parked (issue already locked)', {
|
|
332
|
+
issueId,
|
|
333
|
+
issueIdentifier,
|
|
334
|
+
action,
|
|
335
|
+
workType,
|
|
336
|
+
replaced: result.replaced,
|
|
337
|
+
sessionId: finalSessionId,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
log.info('Work queued successfully', {
|
|
342
|
+
issueId,
|
|
343
|
+
issueIdentifier,
|
|
344
|
+
action,
|
|
345
|
+
workType,
|
|
346
|
+
projectName,
|
|
347
|
+
sessionId: finalSessionId,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
catch (err) {
|
|
352
|
+
log.error('dispatchWork failed', {
|
|
353
|
+
issueId,
|
|
354
|
+
action,
|
|
355
|
+
error: err instanceof Error ? err.message : String(err),
|
|
356
|
+
});
|
|
357
|
+
throw err; // Re-throw so the governor can record the error
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
};
|
|
361
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Governor Logger — Colorized, structured display for the governor CLI.
|
|
3
|
+
*
|
|
4
|
+
* Uses ANSI escape codes directly (matching the core Logger pattern)
|
|
5
|
+
* to avoid external dependencies like chalk.
|
|
6
|
+
*/
|
|
7
|
+
import type { ScanResult } from '@renseiai/agentfactory';
|
|
8
|
+
import type { LinearApiQuota } from '@renseiai/agentfactory-linear';
|
|
9
|
+
export interface StartupBannerConfig {
|
|
10
|
+
version: string;
|
|
11
|
+
projects: string[];
|
|
12
|
+
scanIntervalMs: number;
|
|
13
|
+
maxConcurrentDispatches: number;
|
|
14
|
+
mode: string;
|
|
15
|
+
once: boolean;
|
|
16
|
+
features: {
|
|
17
|
+
autoResearch: boolean;
|
|
18
|
+
autoBacklogCreation: boolean;
|
|
19
|
+
autoDevelopment: boolean;
|
|
20
|
+
autoQA: boolean;
|
|
21
|
+
autoAcceptance: boolean;
|
|
22
|
+
};
|
|
23
|
+
redisConnected: boolean;
|
|
24
|
+
oauthResolved: boolean;
|
|
25
|
+
}
|
|
26
|
+
export declare function printStartupBanner(config: StartupBannerConfig): void;
|
|
27
|
+
export declare function printScanSummary(results: ScanResult[], durationMs: number, quota?: LinearApiQuota, apiCalls?: number): void;
|
|
28
|
+
export declare function printQuotaBar(quota: LinearApiQuota, apiCalls?: number): void;
|
|
29
|
+
export declare function printCircuitBreakerWarning(status: string): void;
|
|
30
|
+
//# sourceMappingURL=governor-logger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"governor-logger.d.ts","sourceRoot":"","sources":["../../../src/lib/governor-logger.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AACxD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAA;AAkCnE,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,EAAE,CAAA;IAClB,cAAc,EAAE,MAAM,CAAA;IACtB,uBAAuB,EAAE,MAAM,CAAA;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,OAAO,CAAA;IACb,QAAQ,EAAE;QACR,YAAY,EAAE,OAAO,CAAA;QACrB,mBAAmB,EAAE,OAAO,CAAA;QAC5B,eAAe,EAAE,OAAO,CAAA;QACxB,MAAM,EAAE,OAAO,CAAA;QACf,cAAc,EAAE,OAAO,CAAA;KACxB,CAAA;IACD,cAAc,EAAE,OAAO,CAAA;IACvB,aAAa,EAAE,OAAO,CAAA;CACvB;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,mBAAmB,GAAG,IAAI,CAqFpE;AAMD,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,UAAU,EAAE,EACrB,UAAU,EAAE,MAAM,EAClB,KAAK,CAAC,EAAE,cAAc,EACtB,QAAQ,CAAC,EAAE,MAAM,GAChB,IAAI,CA6CN;AAMD,wBAAgB,aAAa,CAAC,KAAK,EAAE,cAAc,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAuC5E;AAMD,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAa/D"}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Governor Logger — Colorized, structured display for the governor CLI.
|
|
3
|
+
*
|
|
4
|
+
* Uses ANSI escape codes directly (matching the core Logger pattern)
|
|
5
|
+
* to avoid external dependencies like chalk.
|
|
6
|
+
*/
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// ANSI colors (matching packages/core/src/logger.ts)
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
const c = {
|
|
11
|
+
reset: '\x1b[0m',
|
|
12
|
+
bold: '\x1b[1m',
|
|
13
|
+
dim: '\x1b[2m',
|
|
14
|
+
red: '\x1b[31m',
|
|
15
|
+
green: '\x1b[32m',
|
|
16
|
+
yellow: '\x1b[33m',
|
|
17
|
+
blue: '\x1b[34m',
|
|
18
|
+
magenta: '\x1b[35m',
|
|
19
|
+
cyan: '\x1b[36m',
|
|
20
|
+
white: '\x1b[37m',
|
|
21
|
+
brightBlack: '\x1b[90m',
|
|
22
|
+
brightGreen: '\x1b[92m',
|
|
23
|
+
brightYellow: '\x1b[93m',
|
|
24
|
+
brightRed: '\x1b[91m',
|
|
25
|
+
brightCyan: '\x1b[96m',
|
|
26
|
+
};
|
|
27
|
+
function color(text, ...codes) {
|
|
28
|
+
return `${codes.join('')}${text}${c.reset}`;
|
|
29
|
+
}
|
|
30
|
+
export function printStartupBanner(config) {
|
|
31
|
+
const w = 56; // inner width
|
|
32
|
+
const top = `${c.cyan}┌${'─'.repeat(w)}┐${c.reset}`;
|
|
33
|
+
const bottom = `${c.cyan}└${'─'.repeat(w)}┘${c.reset}`;
|
|
34
|
+
const sep = `${c.cyan}├${'─'.repeat(w)}┤${c.reset}`;
|
|
35
|
+
const pad = (text, rawLen) => {
|
|
36
|
+
const padding = w - 2 - rawLen;
|
|
37
|
+
return `${c.cyan}│${c.reset} ${text}${' '.repeat(Math.max(0, padding))} ${c.cyan}│${c.reset}`;
|
|
38
|
+
};
|
|
39
|
+
const title = 'AgentFactory Governor';
|
|
40
|
+
const titleLen = title.length + config.version.length + 3;
|
|
41
|
+
const titleLine = `${c.bold}${c.white}${title}${c.reset} ${c.dim}v${config.version}${c.reset}`;
|
|
42
|
+
const lines = [
|
|
43
|
+
top,
|
|
44
|
+
pad(titleLine, titleLen),
|
|
45
|
+
sep,
|
|
46
|
+
];
|
|
47
|
+
// Projects
|
|
48
|
+
const projList = config.projects.join(', ');
|
|
49
|
+
lines.push(pad(`${color('Projects:', c.bold)} ${projList}`, 10 + projList.length));
|
|
50
|
+
// Scan interval
|
|
51
|
+
const intervalSec = `${config.scanIntervalMs / 1000}s`;
|
|
52
|
+
lines.push(pad(`${color('Interval:', c.bold)} ${intervalSec}`, 10 + intervalSec.length));
|
|
53
|
+
// Max dispatches
|
|
54
|
+
const maxStr = String(config.maxConcurrentDispatches);
|
|
55
|
+
lines.push(pad(`${color('Max dispatch:', c.bold)} ${maxStr}/scan`, 14 + maxStr.length + 5));
|
|
56
|
+
// Mode
|
|
57
|
+
const modeStr = config.once ? 'single scan' : config.mode;
|
|
58
|
+
lines.push(pad(`${color('Mode:', c.bold)} ${modeStr}`, 6 + modeStr.length));
|
|
59
|
+
lines.push(sep);
|
|
60
|
+
// Feature flags
|
|
61
|
+
const features = config.features;
|
|
62
|
+
const featureEntries = [
|
|
63
|
+
['Research', features.autoResearch],
|
|
64
|
+
['Backlog Creation', features.autoBacklogCreation],
|
|
65
|
+
['Development', features.autoDevelopment],
|
|
66
|
+
['QA', features.autoQA],
|
|
67
|
+
['Acceptance', features.autoAcceptance],
|
|
68
|
+
];
|
|
69
|
+
const enabledList = featureEntries
|
|
70
|
+
.filter(([, v]) => v)
|
|
71
|
+
.map(([k]) => k);
|
|
72
|
+
const disabledList = featureEntries
|
|
73
|
+
.filter(([, v]) => !v)
|
|
74
|
+
.map(([k]) => k);
|
|
75
|
+
if (enabledList.length > 0) {
|
|
76
|
+
const text = enabledList.join(', ');
|
|
77
|
+
lines.push(pad(`${color('Enabled:', c.green, c.bold)} ${text}`, 9 + text.length));
|
|
78
|
+
}
|
|
79
|
+
if (disabledList.length > 0) {
|
|
80
|
+
const text = disabledList.join(', ');
|
|
81
|
+
lines.push(pad(`${color('Disabled:', c.dim)} ${text}`, 10 + text.length));
|
|
82
|
+
}
|
|
83
|
+
lines.push(sep);
|
|
84
|
+
// Integration status
|
|
85
|
+
const redisStatus = config.redisConnected
|
|
86
|
+
? color('connected', c.green)
|
|
87
|
+
: color('not configured', c.yellow);
|
|
88
|
+
const redisRawLen = config.redisConnected ? 16 : 22;
|
|
89
|
+
lines.push(pad(`${color('Redis:', c.bold)} ${redisStatus}`, 7 + (redisRawLen - 7)));
|
|
90
|
+
const oauthStatus = config.oauthResolved
|
|
91
|
+
? color('resolved', c.green)
|
|
92
|
+
: color('personal API key', c.yellow);
|
|
93
|
+
const oauthRawLen = config.oauthResolved ? 15 : 24;
|
|
94
|
+
lines.push(pad(`${color('OAuth:', c.bold)} ${oauthStatus}`, 7 + (oauthRawLen - 7)));
|
|
95
|
+
lines.push(bottom);
|
|
96
|
+
console.log(lines.join('\n'));
|
|
97
|
+
console.log();
|
|
98
|
+
}
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Scan Summary
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
export function printScanSummary(results, durationMs, quota, apiCalls) {
|
|
103
|
+
const ts = formatTime();
|
|
104
|
+
for (const result of results) {
|
|
105
|
+
const { project, scannedIssues, actionsDispatched, skippedReasons, errors } = result;
|
|
106
|
+
const projectTag = color(`[${project}]`, c.cyan, c.bold);
|
|
107
|
+
// Main summary line
|
|
108
|
+
const dispatched = actionsDispatched > 0
|
|
109
|
+
? color(String(actionsDispatched), c.green, c.bold)
|
|
110
|
+
: color('0', c.dim);
|
|
111
|
+
const scanned = color(String(scannedIssues), c.white);
|
|
112
|
+
const skipped = skippedReasons.size > 0
|
|
113
|
+
? color(String(skippedReasons.size), c.dim)
|
|
114
|
+
: '0';
|
|
115
|
+
console.log(`${color(ts, c.dim)} ${projectTag} ${scanned} scanned, ${dispatched} dispatched, ${skipped} skipped`);
|
|
116
|
+
// Errors
|
|
117
|
+
if (errors.length > 0) {
|
|
118
|
+
for (const err of errors) {
|
|
119
|
+
console.log(`${color(ts, c.dim)} ${projectTag} ${color('ERR', c.red)} ${err.issueId}: ${err.error}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Duration
|
|
124
|
+
const durStr = durationMs < 1000
|
|
125
|
+
? `${durationMs}ms`
|
|
126
|
+
: `${(durationMs / 1000).toFixed(1)}s`;
|
|
127
|
+
console.log(`${color(ts, c.dim)} ${color('Scan completed', c.dim)} in ${color(durStr, c.white)}`);
|
|
128
|
+
// Quota bar
|
|
129
|
+
if (quota) {
|
|
130
|
+
printQuotaBar(quota, apiCalls);
|
|
131
|
+
}
|
|
132
|
+
console.log();
|
|
133
|
+
}
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// Quota Bar
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
export function printQuotaBar(quota, apiCalls) {
|
|
138
|
+
const barWidth = 20;
|
|
139
|
+
// Request quota bar
|
|
140
|
+
if (quota.requestsRemaining != null && quota.requestsLimit != null) {
|
|
141
|
+
const used = quota.requestsLimit - quota.requestsRemaining;
|
|
142
|
+
const pct = quota.requestsLimit > 0 ? used / quota.requestsLimit : 0;
|
|
143
|
+
const barColor = getQuotaColor(1 - pct);
|
|
144
|
+
const bar = renderBar(pct, barWidth, barColor);
|
|
145
|
+
const usedStr = used.toLocaleString();
|
|
146
|
+
const limitStr = quota.requestsLimit.toLocaleString();
|
|
147
|
+
const pctStr = `(${Math.round(pct * 100)}%)`;
|
|
148
|
+
let line = ` Linear API ${bar} ${usedStr}/${limitStr} req ${color(pctStr, c.dim)}`;
|
|
149
|
+
if (apiCalls != null) {
|
|
150
|
+
line += ` ${color('│', c.dim)} ${apiCalls} calls this scan`;
|
|
151
|
+
}
|
|
152
|
+
console.log(line);
|
|
153
|
+
}
|
|
154
|
+
// Complexity quota bar
|
|
155
|
+
if (quota.complexityRemaining != null && quota.complexityLimit != null) {
|
|
156
|
+
const used = quota.complexityLimit - quota.complexityRemaining;
|
|
157
|
+
const pct = quota.complexityLimit > 0 ? used / quota.complexityLimit : 0;
|
|
158
|
+
const barColor = getQuotaColor(1 - pct);
|
|
159
|
+
const bar = renderBar(pct, barWidth, barColor);
|
|
160
|
+
const usedStr = formatCompact(used);
|
|
161
|
+
const limitStr = formatCompact(quota.complexityLimit);
|
|
162
|
+
const pctStr = `(${Math.round(pct * 100)}%)`;
|
|
163
|
+
let line = ` ${bar} ${usedStr}/${limitStr} cmplx ${color(pctStr, c.dim)}`;
|
|
164
|
+
if (quota.resetSeconds != null) {
|
|
165
|
+
const resetMin = Math.ceil(quota.resetSeconds / 60);
|
|
166
|
+
line += ` ${color('│', c.dim)} resets in ${resetMin}m`;
|
|
167
|
+
}
|
|
168
|
+
console.log(line);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
// Circuit Breaker Warning
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
export function printCircuitBreakerWarning(status) {
|
|
175
|
+
if (status === 'closed')
|
|
176
|
+
return;
|
|
177
|
+
const ts = formatTime();
|
|
178
|
+
if (status === 'open') {
|
|
179
|
+
console.log(`${color(ts, c.dim)} ${color('CIRCUIT BREAKER OPEN', c.brightRed, c.bold)} — API calls blocked, waiting for reset`);
|
|
180
|
+
}
|
|
181
|
+
else if (status === 'half-open') {
|
|
182
|
+
console.log(`${color(ts, c.dim)} ${color('CIRCUIT BREAKER HALF-OPEN', c.brightYellow, c.bold)} — probing with single request`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
// Helpers
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
function formatTime() {
|
|
189
|
+
const now = new Date();
|
|
190
|
+
return `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`;
|
|
191
|
+
}
|
|
192
|
+
function getQuotaColor(remainingPct) {
|
|
193
|
+
if (remainingPct < 0.10)
|
|
194
|
+
return c.brightRed;
|
|
195
|
+
if (remainingPct < 0.20)
|
|
196
|
+
return c.brightYellow;
|
|
197
|
+
return c.brightGreen;
|
|
198
|
+
}
|
|
199
|
+
function renderBar(pct, width, barColor) {
|
|
200
|
+
const filled = Math.round(pct * width);
|
|
201
|
+
const empty = width - filled;
|
|
202
|
+
return `[${barColor}${'█'.repeat(filled)}${c.dim}${'░'.repeat(empty)}${c.reset}]`;
|
|
203
|
+
}
|
|
204
|
+
function formatCompact(n) {
|
|
205
|
+
if (n >= 1_000_000)
|
|
206
|
+
return `${(n / 1_000_000).toFixed(1)}M`;
|
|
207
|
+
if (n >= 1_000)
|
|
208
|
+
return `${(n / 1_000).toFixed(0)}K`;
|
|
209
|
+
return String(n);
|
|
210
|
+
}
|