@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grantx/fleet-core",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "files": ["src/"],
6
6
  "exports": {
@@ -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
+ }
@@ -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
- log.warn('conductor-loop', `Conductor failed: ${result.message?.slice(0, 200)}`);
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
- log.info('conductor-loop', 'Credentials restored');
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
+ }
@@ -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
- * Validate config schema. Throws on invalid.
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';