@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 +1 -0
- package/engine/claude-models.mjs +13 -0
- package/engine/codex-models.mjs +42 -0
- package/engine/server.mjs +40 -18
- package/engine/worker-availability.mjs +8 -0
- package/package.json +1 -1
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 =
|
|
75
|
-
|
|
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.
|
|
651
|
-
//
|
|
652
|
-
|
|
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.
|
|
1461
|
-
//
|
|
1462
|
-
let workerAuth = { ok:
|
|
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 = {
|
|
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]
|
|
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
|
-
//
|
|
1645
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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