@galda/cli 0.10.51 → 0.10.53

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/app/index.html CHANGED
@@ -7695,6 +7695,7 @@ const AUTH_REASON_TEXT = {
7695
7695
  'logged-out': 'Not signed in to claude. Run `claude` in a terminal and /login, then relaunch the manager from that terminal — tasks are paused until then.',
7696
7696
  'bad-credentials': 'Worker sign-in failed. Check ANTHROPIC_API_KEY, or run `claude` and /login, then relaunch the manager from that terminal.',
7697
7697
  'probe-failed': 'Couldn’t verify claude sign-in. Ensure the `claude` CLI is installed and signed in, then relaunch the manager.',
7698
+ 'rate-limited': 'Claude has reached its usage limit. Claude tasks are queued until access resets; Codex tasks can continue now.',
7698
7699
  };
7699
7700
  function renderAuthBanner(auth){
7700
7701
  const bar = $('errbar');
@@ -0,0 +1,13 @@
1
+ // Claude Code accepts these stable aliases and resolves them to the current
2
+ // model versions for the active account and provider.
3
+ export const CLAUDE_MODELS = Object.freeze([
4
+ 'sonnet',
5
+ 'fable',
6
+ 'best',
7
+ 'opus',
8
+ 'haiku',
9
+ 'default',
10
+ 'sonnet[1m]',
11
+ 'opus[1m]',
12
+ 'opusplan',
13
+ ]);
@@ -0,0 +1,42 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+
5
+ export const FALLBACK_CODEX_MODELS = [
6
+ 'gpt-5.6-sol',
7
+ 'gpt-5.6-terra',
8
+ 'gpt-5.6-luna',
9
+ 'gpt-5.5',
10
+ 'gpt-5.4',
11
+ 'gpt-5.4-mini',
12
+ 'gpt-5.3-codex-spark',
13
+ ];
14
+
15
+ export function parseCodexModelCache(raw, fallback = FALLBACK_CODEX_MODELS) {
16
+ try {
17
+ const parsed = JSON.parse(String(raw));
18
+ const models = Array.isArray(parsed?.models)
19
+ ? parsed.models
20
+ .filter((model) => model?.visibility === 'list' && typeof model.slug === 'string')
21
+ .map((model) => model.slug.trim())
22
+ .filter(Boolean)
23
+ : [];
24
+ return models.length ? [...new Set(models)] : fallback.slice();
25
+ } catch {
26
+ return fallback.slice();
27
+ }
28
+ }
29
+
30
+ // Codex refreshes this cache itself. Reading it gives Galda the account's real
31
+ // picker catalog without launching Codex, making a network request, or running
32
+ // an installer. Older CLIs and first-run installs simply use the fallback.
33
+ export function loadCodexModels({ cacheFile, readFile = readFileSync } = {}) {
34
+ const file = cacheFile
35
+ || process.env.MANAGER_CODEX_MODELS_FILE
36
+ || join(process.env.CODEX_HOME || join(homedir(), '.codex'), 'models_cache.json');
37
+ try {
38
+ return parseCodexModelCache(readFile(file, 'utf8'));
39
+ } catch {
40
+ return FALLBACK_CODEX_MODELS.slice();
41
+ }
42
+ }
package/engine/server.mjs CHANGED
@@ -20,10 +20,13 @@ import { resolve, dirname, join, basename } from 'node:path';
20
20
  import { homedir } from 'node:os';
21
21
  import { fileURLToPath } from 'node:url';
22
22
  import { pathToFileURL } from 'node:url';
23
- import { needsProjectFolder, folderLabel, chooseFolderScript, parseChosenFolder, buildFolderQuestion, resolveFolderAnswer, classifyFolderTarget, buildFolderInitConfirm, isFolderInitConfirmed, needsReviewPreference, buildReviewPreferenceQuestion, resolveReviewPreferenceAnswer, routeQuestionAnswer, notUnderstoodNote, parseRelocalizedQuestion, classifyAuthProbe, authBannerText, makeSigninChallenge, parseClaimResponse } from './lib.mjs';
23
+ import { needsProjectFolder, folderLabel, chooseFolderScript, parseChosenFolder, buildFolderQuestion, resolveFolderAnswer, classifyFolderTarget, buildFolderInitConfirm, isFolderInitConfirmed, needsReviewPreference, buildReviewPreferenceQuestion, resolveReviewPreferenceAnswer, routeQuestionAnswer, notUnderstoodNote, parseRelocalizedQuestion, classifyAuthProbe, authBannerText, makeSigninChallenge, parseClaimResponse, isCommandOnPath } from './lib.mjs';
24
24
  import { parseStreamEvents, parseCodexEvents, buildCodexArgs, parsePlan, refinePlanTasks, canEditGoal, canDeleteGoal, shouldAskClarification, workerExitReason, isForcedStop, taskStatusAfterVerify, workerResultText, taskCountsAsComplete, resolveWantsPR, resolveGoalSource, buildGoalSummary, buildGoalProofMd, buildGoalPrBody, buildReviewSummaryPrompt, parseReviewSummary, buildReviewRuleSection, nextGoalStatus, isPrMerged, isPrApproved, reviewToDoneStatus, advanceReviewGoal, revertReviewGoal, approveGoal, dismissGoal, rejectGoal, reopenGoal, permissionModeFor, isPlanReview, nextAfterPlanApprove, GOAL_MODES, parseSkillFrontmatter, collectSkills, retestGoal, cancelRetestGoal, computeRetestOutcome, revertGoal, planRevertActions, archiveGoal, unarchiveGoal, undismissGoal, parseNumstat, truncateDiffText, sumUsage, resolveEntryUrl, parseGitLog, diffNewCommits, replayQueueLog, reconcileOrphanGoals, reorderQueue, sortQueueByPriority, TASK_PRIORITIES, validateWorkflowColumns, DEFAULT_WORKFLOW_COLUMNS, validateReviewDefinition, DEFAULT_REVIEW_DEFINITION, resolveTestGate, buildEphemeralSeedLog, buildEphemeralSeedProjects, trimTaskActivityForState, buildAskContext, WORKER_TOOLS, workerPrompt, verifyCfAccessJwt, resolveIdentity, goalVisibleTo, clampParallelLimit, canStartMore, nextRunnableTasks, goalsConflict, detectConflicts, pickFoldTarget, hasTestRelevantChanges, parseFailingTestNames, classifyTestGate, isInconclusiveTestRun, countInfraFlakes, classifyRunFailures, classifyVerifyGate, buildExecutionPlan, buildContextHandoffSummary, buildFailurePostmortem, appendFailureMemory, latestFailurePolicy, classifyChangeRisk, checkRunBudget, usageBudgetTokens, isRateLimited, nextResumeDelay, checkFreeTierLimit, resolveEntitlement, resolveCachedEntitlement, FREE_TIER_LIMITS, QUEUED_GOAL_STATUSES, verifyLicenseToken, licenseTokenPayload, shouldRefreshLicense, shouldEmitSetupCompleted, pickAnalyticsUid, shouldEmitFreeExhausted, detectRequestLanguage, isNothingVerifiable, classifyComposerIntentHeuristic, detectPauseIntent, buildIntentPrompt, parseIntentResponse, buildBoardSnapshot, validateBoardSnapshot, boardIsEmpty, decideBoardPull, resolveConnectedAgents, pickAvailableAgent, pickUtilityAgent, utilityModel, liveTakeoverDecision, shouldOpenBrowserOnBoot, goalTaskTitle, buildGoalTask, RUN_DECL_FILE, parseRunDeclaration, SHOT_DIR, collectShotNames, parseGoalAddress, REPLY_OUTCOMES, REPLY_INTENTS, buildReplyIntentPrompt, parseReplyIntent, buildGoalMessage, serializeGoalMessage, parseGoalMessages } from './lib.mjs';
25
25
  import { createSerialQueue } from './lib.mjs';
26
26
  import { collectProjectRules } from './lib.mjs';
27
+ import { FALLBACK_CODEX_MODELS, loadCodexModels } from './codex-models.mjs';
28
+ import { CLAUDE_MODELS } from './claude-models.mjs';
29
+ import { classifyWorkerPreflight, workerCanStart } from './worker-availability.mjs';
27
30
  import { openPR } from './pr.mjs';
28
31
  import { runVerification, exerciseUi } from './verify.mjs';
29
32
 
@@ -71,8 +74,8 @@ const WORKER_TIMEOUT_MS = Number(process.env.MANAGER_WORKER_TIMEOUT_MS) || 15 *
71
74
  // review card showed a truncated report and called it evidence. Keep a cap —
72
75
  // this lands in persisted state — but one that fits a real answer.
73
76
  const RESULT_MAX = 20000;
74
- const MODELS = ['sonnet', 'opus', 'haiku'];
75
- const CODEX_MODELS = ['gpt-5.5', 'gpt-5.4', 'gpt-5.4-mini', 'gpt-5.3-codex-spark'];
77
+ const MODELS = CLAUDE_MODELS;
78
+ let CODEX_MODELS = FALLBACK_CODEX_MODELS.slice();
76
79
  const WORKER_AGENTS = ['claude-code', 'codex'];
77
80
  const CLAUDE_EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max'];
78
81
  const CODEX_EFFORTS = ['low', 'medium', 'high', 'xhigh'];
@@ -647,14 +650,25 @@ let _syncDirtyAgain = false; // a save landed mid-push → coalesce into
647
650
  // Which worker agent CLIs are installed on this machine — the launcher
648
651
  // (bin/manager-for-ai.mjs) detects them (codex via a PATH lookup, never executed)
649
652
  // and passes the set here. Galda offers only these, so a Codex-only machine gets
650
- // a working board and a claude-only one never shows Codex. Defaults to Claude
651
- // Code when unset (tests / running the server directly).
652
- const envAgents = (process.env.MANAGER_AVAILABLE_AGENTS || 'claude-code')
653
+ // a working board and a claude-only one never shows Codex.
654
+ // The packaged launcher sets MANAGER_AVAILABLE_AGENTS after its safe CLI probe.
655
+ // Direct starts (launchd, `npm start`, development) bypass that launcher, so do
656
+ // the same non-executing PATH lookup here instead of silently becoming
657
+ // Claude-only. Never invoke an agent, npx, or an installer during detection.
658
+ const configuredAgents = process.env.MANAGER_AVAILABLE_AGENTS;
659
+ const detectedAgents = configuredAgents === undefined
660
+ ? [
661
+ isCommandOnPath('claude', { pathEnv: process.env.PATH || '', sep: process.platform === 'win32' ? ';' : ':', pathExt: process.env.PATHEXT || '' }) && 'claude-code',
662
+ isCommandOnPath('codex', { pathEnv: process.env.PATH || '', sep: process.platform === 'win32' ? ';' : ':', pathExt: process.env.PATHEXT || '' }) && 'codex',
663
+ ].filter(Boolean).join(',')
664
+ : configuredAgents;
665
+ const envAgents = (detectedAgents || 'claude-code')
653
666
  .split(',').map((a) => a.trim()).filter(Boolean);
654
667
  const AVAILABLE_AGENTS = resolveConnectedAgents({
655
668
  claude: envAgents.includes('claude-code'),
656
669
  codex: envAgents.includes('codex'),
657
670
  });
671
+ if (AVAILABLE_AGENTS.includes('codex')) CODEX_MODELS = loadCodexModels();
658
672
 
659
673
  // ---- state ---------------------------------------------------------------
660
674
  // Restarts must not lose pending work: tasks that never started stay
@@ -1457,9 +1471,9 @@ function startNextStacked(projectId) {
1457
1471
  // standalone `claude` (runClaude), inheriting the launching shell's login. If
1458
1472
  // that shell has no usable credentials, every task fails late with "Not logged
1459
1473
  // in". We probe once at boot and hold the queue + raise a banner instead of
1460
- // failing task after task. Optimistic default (ok:true) so we never falsely
1461
- // block a correctly-authed setup during the ~1s probe.
1462
- let workerAuth = { ok: true, reason: null, checkedAt: null };
1474
+ // failing task after task. Claude waits for the short probe; Codex is gated
1475
+ // independently and may start immediately.
1476
+ let workerAuth = { ok: null, reason: null, checkedAt: null };
1463
1477
  const publicAuth = () => ({ ok: workerAuth.ok, reason: workerAuth.reason });
1464
1478
 
1465
1479
  // Boot preflight: one cheap Read-only `claude` call; classify the result (pure,
@@ -1468,14 +1482,18 @@ const publicAuth = () => ({ ok: workerAuth.ok, reason: workerAuth.reason });
1468
1482
  async function probeWorkerAuth() {
1469
1483
  try {
1470
1484
  const out = await runClaude({ prompt: 'Reply with the single word: ok', cwd: ROOT, tools: 'Read', model: 'haiku' });
1471
- workerAuth = { ...classifyAuthProbe({ result: out?.result, code: out?.code }), checkedAt: Date.now() };
1485
+ workerAuth = {
1486
+ ...classifyWorkerPreflight(out, { classifyAuthProbe, isRateLimited }),
1487
+ checkedAt: Date.now(),
1488
+ };
1472
1489
  } catch {
1473
1490
  workerAuth = { ok: false, reason: 'probe-failed', checkedAt: Date.now() };
1474
1491
  }
1475
1492
  console.log(workerAuth.ok
1476
1493
  ? '[manager] worker auth preflight OK'
1477
- : `[manager] worker auth preflight FAILED (${workerAuth.reason}) — holding the queue. ${authBannerText(workerAuth.reason, 'en')}`);
1494
+ : `[manager] Claude preflight unavailable (${workerAuth.reason}) — holding Claude tasks; Codex may continue.`);
1478
1495
  send({ ev: 'auth', auth: publicAuth() });
1496
+ if (workerAuth.ok) for (const project of projects) pump(project.id);
1479
1497
  return workerAuth;
1480
1498
  }
1481
1499
 
@@ -1641,10 +1659,8 @@ async function authorVerify(task, goal, project) {
1641
1659
  // `lastStartAt` check in pump() below.
1642
1660
  const slowModeLastStart = new Map(); // projectId -> ms epoch of the last task start
1643
1661
  function pump(projectId) {
1644
- // Worker auth preflight failed hold the queue (the UI banner tells the user
1645
- // to sign `claude` in and relaunch). Never spawn a worker that can only fail
1646
- // with "Not logged in". Cleared automatically once a probe succeeds.
1647
- if (!workerAuth.ok) return;
1662
+ // A Claude sign-in or usage hold must not stop an installed Codex worker.
1663
+ // Claude work stays queued until a later probe succeeds.
1648
1664
  // Project paused by the user (Doing列の一時停止 / chat) → hold this project's
1649
1665
  // queue exactly like the auth hold above. Resume flips the flag and pumps.
1650
1666
  if (isProjectPaused(projectId)) return;
@@ -1671,7 +1687,8 @@ function pump(projectId) {
1671
1687
  if (wait > 0) { setTimeout(() => pump(projectId), wait); return; }
1672
1688
  }
1673
1689
  const runningGoalIds = [...q.running].map((t) => t.goalId);
1674
- const toStart = nextRunnableTasks(q.waiting, runningGoalIds, slow.enabled ? 1 : slots);
1690
+ const eligible = q.waiting.filter((task) => workerCanStart({ agent: task.agent, auth: workerAuth }));
1691
+ const toStart = nextRunnableTasks(eligible, runningGoalIds, slow.enabled ? 1 : slots);
1675
1692
  for (const task of toStart) {
1676
1693
  const i = q.waiting.indexOf(task);
1677
1694
  if (i !== -1) q.waiting.splice(i, 1);
@@ -1968,7 +1985,9 @@ function queueReplyTask(goal, text, { from = 'manager', via = 'composer' } = {})
1968
1985
  const projectCap = slow.enabled ? Math.min(PROJECT_MAX_PARALLEL, slow.maxParallel) : PROJECT_MAX_PARALLEL;
1969
1986
  const slots = Math.min(projectCap - q.running.size, GLOBAL_MAX_PARALLEL - globalRunningCount());
1970
1987
  const runningGoalIds = [...q.running].map((t) => t.goalId);
1971
- const startsNow = nextRunnableTasks(q.waiting, runningGoalIds, slow.enabled ? 1 : slots).some((t) => t.id === task.id);
1988
+ const eligible = q.waiting.filter((candidate) => workerCanStart({ agent: candidate.agent, auth: workerAuth }));
1989
+ const startsNow = workerCanStart({ agent: task.agent, auth: workerAuth })
1990
+ && nextRunnableTasks(eligible, runningGoalIds, slow.enabled ? 1 : slots).some((candidate) => candidate.id === task.id);
1972
1991
  goal.startedAt = startsNow ? new Date().toISOString() : null;
1973
1992
  saveGoal(goal);
1974
1993
  }
@@ -4900,7 +4919,10 @@ server.listen(PORT, '127.0.0.1', () => {
4900
4919
  spawn('open', ['-g', openUrl], { stdio: 'ignore' }).unref();
4901
4920
  }
4902
4921
  console.log(`[manager] projects: ${projects.map((p) => `${p.id}=${p.dir}`).join(' ')}`);
4903
- probeWorkerAuth().catch(() => {}); // hold the queue + banner if `claude` isn't signed in
4922
+ probeWorkerAuth().catch(() => {}); // hold Claude tasks + banner if Claude is unavailable
4923
+ setInterval(() => {
4924
+ if (!workerAuth.ok) probeWorkerAuth().catch(() => {});
4925
+ }, 5 * 60_000).unref();
4904
4926
  syncAllExternal().catch(() => {});
4905
4927
  syncGoalMerges().catch(() => {});
4906
4928
  setInterval(() => {
@@ -0,0 +1,8 @@
1
+ export function classifyWorkerPreflight({ result = '', code = 0 } = {}, { classifyAuthProbe, isRateLimited }) {
2
+ if (isRateLimited(result)) return { ok: false, reason: 'rate-limited' };
3
+ return classifyAuthProbe({ result, code });
4
+ }
5
+
6
+ export function workerCanStart({ agent, auth } = {}) {
7
+ return auth?.ok === true || agent === 'codex';
8
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@galda/cli",
3
- "version": "0.10.51",
3
+ "version": "0.10.53",
4
4
  "type": "module",
5
5
  "description": "Galda - hand off work to Claude Code or Codex, get proof back. Runs on your existing subscription, no extra API cost.",
6
6
  "scripts": {