@grantx/fleet-core 0.1.3 → 0.1.5
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/package.json +1 -1
- package/src/agent-runner.js +19 -0
- package/src/conductor-loop.js +41 -3
- package/src/conductor-runner.js +1 -1
- package/src/config.js +20 -3
- package/src/index.js +1 -1
package/package.json
CHANGED
package/src/agent-runner.js
CHANGED
|
@@ -174,6 +174,15 @@ export class AgentPool {
|
|
|
174
174
|
let output = stdout.trim();
|
|
175
175
|
const errText = stderr.trim();
|
|
176
176
|
|
|
177
|
+
// Auth failure: bail immediately, no retry
|
|
178
|
+
if (code === 1 && _isAuthError(errText) || _isAuthError(output)) {
|
|
179
|
+
const reason = 'OAuth token expired — run `claude login` on host';
|
|
180
|
+
log.error('agent-runner', `${agent}: auth failure, not retrying: ${(errText || output).slice(0, 200)}`);
|
|
181
|
+
resolve({ ok: false, message: reason, authFailed: true });
|
|
182
|
+
this._drainQueue();
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
177
186
|
// Self-healing: wrong session flag -> retry once. On second failure, fresh session.
|
|
178
187
|
if (code === 1 && retryCount < 2) {
|
|
179
188
|
if (errText.includes('already in use') && !isInitialized) {
|
|
@@ -289,3 +298,13 @@ export class AgentPool {
|
|
|
289
298
|
this.queue = [];
|
|
290
299
|
}
|
|
291
300
|
}
|
|
301
|
+
|
|
302
|
+
// ── Auth error detection ───────────────────────────────────────────
|
|
303
|
+
|
|
304
|
+
const AUTH_ERROR_PATTERNS = ['401', 'oauth', 'expired', 'authentication_error', 'token has expired'];
|
|
305
|
+
|
|
306
|
+
function _isAuthError(text) {
|
|
307
|
+
if (!text) return false;
|
|
308
|
+
const lower = text.toLowerCase();
|
|
309
|
+
return AUTH_ERROR_PATTERNS.some(p => lower.includes(p));
|
|
310
|
+
}
|
package/src/conductor-loop.js
CHANGED
|
@@ -38,6 +38,8 @@ export class ConductorLoop {
|
|
|
38
38
|
this._lastSynthesis = Date.now();
|
|
39
39
|
this._lastCredCheck = Date.now();
|
|
40
40
|
this._credHealthy = true;
|
|
41
|
+
this._authFailed = false; // Circuit breaker: true = skip ticks until creds recover
|
|
42
|
+
this._consecutiveFailures = 0;
|
|
41
43
|
|
|
42
44
|
// Stats
|
|
43
45
|
this.stats = {
|
|
@@ -94,6 +96,7 @@ export class ConductorLoop {
|
|
|
94
96
|
return {
|
|
95
97
|
...this.stats,
|
|
96
98
|
running: this._running,
|
|
99
|
+
authFailed: this._authFailed,
|
|
97
100
|
eventQueueDepth: this._events.length,
|
|
98
101
|
uptime: this.stats.startTime ? Math.round((Date.now() - this.stats.startTime) / 1000) : 0,
|
|
99
102
|
dispatcher: this.dispatcher.getStats(),
|
|
@@ -110,9 +113,14 @@ export class ConductorLoop {
|
|
|
110
113
|
try {
|
|
111
114
|
this.stats.ticks++;
|
|
112
115
|
|
|
113
|
-
// Inject cron events
|
|
116
|
+
// Inject cron events (always runs — needed for credential recovery check)
|
|
114
117
|
this._checkCrons();
|
|
115
118
|
|
|
119
|
+
// Circuit breaker: skip work when auth is known-broken
|
|
120
|
+
if (this._authFailed) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
116
124
|
// Detect state changes from Supabase
|
|
117
125
|
await this._detectStateChanges();
|
|
118
126
|
|
|
@@ -270,10 +278,26 @@ export class ConductorLoop {
|
|
|
270
278
|
const result = await this.conductor.invoke(prompt);
|
|
271
279
|
|
|
272
280
|
if (!result.ok) {
|
|
273
|
-
|
|
281
|
+
const msg = result.message || '';
|
|
282
|
+
this._consecutiveFailures++;
|
|
283
|
+
const isAuth = _isAuthError(msg);
|
|
284
|
+
// Treat 5+ consecutive failures as likely auth issue even without keyword match
|
|
285
|
+
const likelyAuth = !isAuth && this._consecutiveFailures >= 5 && msg.includes('code 1');
|
|
286
|
+
if (isAuth || likelyAuth) {
|
|
287
|
+
this._authFailed = true;
|
|
288
|
+
this._credHealthy = false;
|
|
289
|
+
const reason = isAuth ? msg.slice(0, 200) : `${this._consecutiveFailures} consecutive failures (likely auth)`;
|
|
290
|
+
log.error('conductor-loop', `AUTH FAILURE — pausing loop until credentials recover: ${reason}`);
|
|
291
|
+
this.pushEvent({ type: 'credential_expired', reason, timestamp: new Date().toISOString() });
|
|
292
|
+
} else {
|
|
293
|
+
log.warn('conductor-loop', `Conductor failed: ${msg.slice(0, 200)}`);
|
|
294
|
+
}
|
|
274
295
|
return;
|
|
275
296
|
}
|
|
276
297
|
|
|
298
|
+
// Reset consecutive failure counter on success
|
|
299
|
+
this._consecutiveFailures = 0;
|
|
300
|
+
|
|
277
301
|
// Parse dispatch block
|
|
278
302
|
const block = parseDispatchBlock(result.output);
|
|
279
303
|
if (!block) {
|
|
@@ -332,7 +356,12 @@ export class ConductorLoop {
|
|
|
332
356
|
});
|
|
333
357
|
} else if (result.ok && !this._credHealthy) {
|
|
334
358
|
this._credHealthy = true;
|
|
335
|
-
|
|
359
|
+
if (this._authFailed) {
|
|
360
|
+
this._authFailed = false;
|
|
361
|
+
log.info('conductor-loop', 'Credentials restored — resuming loop');
|
|
362
|
+
} else {
|
|
363
|
+
log.info('conductor-loop', 'Credentials restored');
|
|
364
|
+
}
|
|
336
365
|
}
|
|
337
366
|
} catch (err) {
|
|
338
367
|
log.warn('conductor-loop', `Credential check error: ${err.message}`);
|
|
@@ -364,3 +393,12 @@ export class ConductorLoop {
|
|
|
364
393
|
}
|
|
365
394
|
}
|
|
366
395
|
}
|
|
396
|
+
|
|
397
|
+
// ── Auth error detection ───────────────────────────────────────────
|
|
398
|
+
|
|
399
|
+
const AUTH_ERROR_PATTERNS = ['401', 'oauth', 'expired', 'authentication_error', 'token has expired'];
|
|
400
|
+
|
|
401
|
+
function _isAuthError(message) {
|
|
402
|
+
const lower = message.toLowerCase();
|
|
403
|
+
return AUTH_ERROR_PATTERNS.some(p => lower.includes(p));
|
|
404
|
+
}
|
package/src/conductor-runner.js
CHANGED
|
@@ -129,7 +129,7 @@ export class ConductorRunner {
|
|
|
129
129
|
log.info('conductor-runner', `Conductor completed (${output.length} chars)`);
|
|
130
130
|
resolve({ ok: true, output });
|
|
131
131
|
} else {
|
|
132
|
-
const errMsg = errText.slice(0, 500) || 'No output';
|
|
132
|
+
const errMsg = errText.slice(0, 500) || output.slice(0, 500) || 'No output';
|
|
133
133
|
log.warn('conductor-runner', `Conductor exited with code ${code}: ${errMsg.slice(0, 200)}`);
|
|
134
134
|
resolve({ ok: false, message: `Conductor exited with code ${code}: ${errMsg}` });
|
|
135
135
|
}
|
package/src/config.js
CHANGED
|
@@ -50,11 +50,18 @@ function findConfigFile(dir) {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
|
-
*
|
|
53
|
+
* Valid team IDs. Used by init.js and validated here.
|
|
54
|
+
*/
|
|
55
|
+
export const VALID_TEAM_IDS = ['grantx-ml', 'grantx-data', 'grantx-fullstack', 'grantx-skunkworks'];
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Validate config schema. Supports version 1 and 2.
|
|
59
|
+
* v1: original schema (auto-generated agents)
|
|
60
|
+
* v2: rich agents with identity, domains, frozenDecisions, repos, slack
|
|
54
61
|
*/
|
|
55
62
|
function validate(config, filePath) {
|
|
56
|
-
if (config.version !== 1) {
|
|
57
|
-
throw new Error(`${filePath}: unsupported version ${config.version} (expected 1)`);
|
|
63
|
+
if (config.version !== 1 && config.version !== 2) {
|
|
64
|
+
throw new Error(`${filePath}: unsupported version ${config.version} (expected 1 or 2)`);
|
|
58
65
|
}
|
|
59
66
|
if (!config.teamId || typeof config.teamId !== 'string') {
|
|
60
67
|
throw new Error(`${filePath}: teamId is required (string)`);
|
|
@@ -79,6 +86,16 @@ function validate(config, filePath) {
|
|
|
79
86
|
throw new Error(`${filePath}: agent "${agent.name}" must have a role (string)`);
|
|
80
87
|
}
|
|
81
88
|
}
|
|
89
|
+
|
|
90
|
+
// v2-specific: repos and slack are optional arrays/objects — validate shape if present
|
|
91
|
+
if (config.repos && !Array.isArray(config.repos)) {
|
|
92
|
+
throw new Error(`${filePath}: repos must be an array`);
|
|
93
|
+
}
|
|
94
|
+
if (config.slack) {
|
|
95
|
+
if (config.slack.botToken && typeof config.slack.botToken !== 'string') {
|
|
96
|
+
throw new Error(`${filePath}: slack.botToken must be a string`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
82
99
|
}
|
|
83
100
|
|
|
84
101
|
/**
|
package/src/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @grantx/fleet-core — shared engine for the fleet plugin
|
|
2
2
|
// Cross-platform, config-driven, no hardcoded Grantx references.
|
|
3
3
|
|
|
4
|
-
export { loadConfig, getAgentRoster, getConductor, getWorkerAgents } from './config.js';
|
|
4
|
+
export { loadConfig, getAgentRoster, getConductor, getWorkerAgents, VALID_TEAM_IDS } from './config.js';
|
|
5
5
|
export { AgentPool } from './agent-runner.js';
|
|
6
6
|
export { ConductorRunner, parseDispatchBlock } from './conductor-runner.js';
|
|
7
7
|
export { SmartDispatcher } from './smart-dispatcher.js';
|