@galda/cli 0.10.52 → 0.10.54

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
@@ -6894,16 +6894,17 @@ function renderFsTodo(){
6894
6894
  // (only a reply task, which Doing excludes) — it used to vanish from the lane (Masa 2026-07-24).
6895
6895
  // Show it in Doing as a goal row, and stop its old stopped task from lingering under "Needs you".
6896
6896
  const runningTaskGoalIds = new Set(running.filter((t) => !t.reply).map((t) => t.goalId));
6897
- const resumedGoalIds = new Set(pgoals.filter((g) => g.status === 'running' && g.startedAt).map((g) => g.id));
6897
+ const runningReplyGoalIds = new Set(list.filter((t) => t.reply && t.status === 'running').map((t) => t.goalId));
6898
+ const resumedGoalIds = new Set(runningReplyGoalIds);
6898
6899
  const doingGoals = pgoals.filter((g) => g.status === 'running' && g.startedAt
6899
- && !runningTaskGoalIds.has(g.id) && !pausedGoalIds.has(g.id));
6900
+ && runningReplyGoalIds.has(g.id) && !runningTaskGoalIds.has(g.id) && !pausedGoalIds.has(g.id));
6900
6901
  // A reply that was OVER the parallel cap (engine PR #342): the goal is 'running'
6901
6902
  // but startedAt is cleared, and its only pending work is a QUEUED reply task —
6902
6903
  // which Doing needs startedAt for, and Next-up excludes (!t.reply). With no
6903
6904
  // bucket it vanished. Surface these "parked" goals at the TOP of To Do ("最上部",
6904
6905
  // Masa 2026-07-24) as Queued rows — not "Working on it" (nothing runs yet). They
6905
6906
  // auto-promote to Doing the moment a slot frees and the engine sets startedAt.
6906
- const parkedReplyGoals = pgoals.filter((g) => g.status === 'running' && !g.startedAt
6907
+ const parkedReplyGoals = pgoals.filter((g) => g.status === 'running'
6907
6908
  && !pausedGoalIds.has(g.id) && !runningTaskGoalIds.has(g.id)
6908
6909
  && list.some((t) => t.goalId === g.id && t.reply && t.status === 'queued')
6909
6910
  && !list.some((t) => t.goalId === g.id && !t.reply && ['running', 'queued'].includes(t.status)));
@@ -7695,6 +7696,7 @@ const AUTH_REASON_TEXT = {
7695
7696
  '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
7697
  'bad-credentials': 'Worker sign-in failed. Check ANTHROPIC_API_KEY, or run `claude` and /login, then relaunch the manager from that terminal.',
7697
7698
  'probe-failed': 'Couldn’t verify claude sign-in. Ensure the `claude` CLI is installed and signed in, then relaunch the manager.',
7699
+ 'rate-limited': 'Claude has reached its usage limit. Claude tasks are queued until access resets; Codex tasks can continue now.',
7698
7700
  };
7699
7701
  function renderAuthBanner(auth){
7700
7702
  const bar = $('errbar');
package/engine/server.mjs CHANGED
@@ -26,6 +26,7 @@ import { createSerialQueue } from './lib.mjs';
26
26
  import { collectProjectRules } from './lib.mjs';
27
27
  import { FALLBACK_CODEX_MODELS, loadCodexModels } from './codex-models.mjs';
28
28
  import { CLAUDE_MODELS } from './claude-models.mjs';
29
+ import { classifyWorkerPreflight, workerCanStart } from './worker-availability.mjs';
29
30
  import { openPR } from './pr.mjs';
30
31
  import { runVerification, exerciseUi } from './verify.mjs';
31
32
 
@@ -1101,6 +1102,7 @@ function rebuildQueuesAfterAdopt() {
1101
1102
  for (const p of projects) queues.set(p.id, { running: new Set(), waiting: [] });
1102
1103
  for (const p of projects) {
1103
1104
  const pending = tasks.filter((t) => t.projectId === p.id && t.status === 'queued').sort((a, b) => a.num - b.num);
1105
+ reconcileQueuedGoalStarts(p.id);
1104
1106
  queues.get(p.id).waiting.push(...pending);
1105
1107
  pump(p.id);
1106
1108
  for (const g of goals.filter((g) => g.projectId === p.id && g.status === 'stacked')) {
@@ -1110,6 +1112,17 @@ function rebuildQueuesAfterAdopt() {
1110
1112
  }
1111
1113
  }
1112
1114
 
1115
+ function reconcileQueuedGoalStarts(projectId) {
1116
+ const projectTasks = tasks.filter((task) => task.projectId === projectId);
1117
+ for (const goal of goals.filter((item) => item.projectId === projectId && item.status === 'running' && item.startedAt)) {
1118
+ const goalTasks = projectTasks.filter((task) => task.goalId === goal.id);
1119
+ if (goalTasks.some((task) => task.status === 'queued') && !goalTasks.some((task) => task.status === 'running')) {
1120
+ goal.startedAt = null;
1121
+ saveGoal(goal);
1122
+ }
1123
+ }
1124
+ }
1125
+
1113
1126
  const ACTIVITY_LIMIT = 1000;
1114
1127
  function activityLine(line) {
1115
1128
  return String(line ?? '')
@@ -1470,9 +1483,9 @@ function startNextStacked(projectId) {
1470
1483
  // standalone `claude` (runClaude), inheriting the launching shell's login. If
1471
1484
  // that shell has no usable credentials, every task fails late with "Not logged
1472
1485
  // in". We probe once at boot and hold the queue + raise a banner instead of
1473
- // failing task after task. Optimistic default (ok:true) so we never falsely
1474
- // block a correctly-authed setup during the ~1s probe.
1475
- let workerAuth = { ok: true, reason: null, checkedAt: null };
1486
+ // failing task after task. Claude waits for the short probe; Codex is gated
1487
+ // independently and may start immediately.
1488
+ let workerAuth = { ok: null, reason: null, checkedAt: null };
1476
1489
  const publicAuth = () => ({ ok: workerAuth.ok, reason: workerAuth.reason });
1477
1490
 
1478
1491
  // Boot preflight: one cheap Read-only `claude` call; classify the result (pure,
@@ -1481,14 +1494,18 @@ const publicAuth = () => ({ ok: workerAuth.ok, reason: workerAuth.reason });
1481
1494
  async function probeWorkerAuth() {
1482
1495
  try {
1483
1496
  const out = await runClaude({ prompt: 'Reply with the single word: ok', cwd: ROOT, tools: 'Read', model: 'haiku' });
1484
- workerAuth = { ...classifyAuthProbe({ result: out?.result, code: out?.code }), checkedAt: Date.now() };
1497
+ workerAuth = {
1498
+ ...classifyWorkerPreflight(out, { classifyAuthProbe, isRateLimited }),
1499
+ checkedAt: Date.now(),
1500
+ };
1485
1501
  } catch {
1486
1502
  workerAuth = { ok: false, reason: 'probe-failed', checkedAt: Date.now() };
1487
1503
  }
1488
1504
  console.log(workerAuth.ok
1489
1505
  ? '[manager] worker auth preflight OK'
1490
- : `[manager] worker auth preflight FAILED (${workerAuth.reason}) — holding the queue. ${authBannerText(workerAuth.reason, 'en')}`);
1506
+ : `[manager] Claude preflight unavailable (${workerAuth.reason}) — holding Claude tasks; Codex may continue.`);
1491
1507
  send({ ev: 'auth', auth: publicAuth() });
1508
+ if (workerAuth.ok) for (const project of projects) pump(project.id);
1492
1509
  return workerAuth;
1493
1510
  }
1494
1511
 
@@ -1654,10 +1671,8 @@ async function authorVerify(task, goal, project) {
1654
1671
  // `lastStartAt` check in pump() below.
1655
1672
  const slowModeLastStart = new Map(); // projectId -> ms epoch of the last task start
1656
1673
  function pump(projectId) {
1657
- // Worker auth preflight failed hold the queue (the UI banner tells the user
1658
- // to sign `claude` in and relaunch). Never spawn a worker that can only fail
1659
- // with "Not logged in". Cleared automatically once a probe succeeds.
1660
- if (!workerAuth.ok) return;
1674
+ // A Claude sign-in or usage hold must not stop an installed Codex worker.
1675
+ // Claude work stays queued until a later probe succeeds.
1661
1676
  // Project paused by the user (Doing列の一時停止 / chat) → hold this project's
1662
1677
  // queue exactly like the auth hold above. Resume flips the flag and pumps.
1663
1678
  if (isProjectPaused(projectId)) return;
@@ -1684,7 +1699,8 @@ function pump(projectId) {
1684
1699
  if (wait > 0) { setTimeout(() => pump(projectId), wait); return; }
1685
1700
  }
1686
1701
  const runningGoalIds = [...q.running].map((t) => t.goalId);
1687
- const toStart = nextRunnableTasks(q.waiting, runningGoalIds, slow.enabled ? 1 : slots);
1702
+ const eligible = q.waiting.filter((task) => workerCanStart({ agent: task.agent, auth: workerAuth }));
1703
+ const toStart = nextRunnableTasks(eligible, runningGoalIds, slow.enabled ? 1 : slots);
1688
1704
  for (const task of toStart) {
1689
1705
  const i = q.waiting.indexOf(task);
1690
1706
  if (i !== -1) q.waiting.splice(i, 1);
@@ -1981,7 +1997,9 @@ function queueReplyTask(goal, text, { from = 'manager', via = 'composer' } = {})
1981
1997
  const projectCap = slow.enabled ? Math.min(PROJECT_MAX_PARALLEL, slow.maxParallel) : PROJECT_MAX_PARALLEL;
1982
1998
  const slots = Math.min(projectCap - q.running.size, GLOBAL_MAX_PARALLEL - globalRunningCount());
1983
1999
  const runningGoalIds = [...q.running].map((t) => t.goalId);
1984
- const startsNow = nextRunnableTasks(q.waiting, runningGoalIds, slow.enabled ? 1 : slots).some((t) => t.id === task.id);
2000
+ const eligible = q.waiting.filter((candidate) => workerCanStart({ agent: candidate.agent, auth: workerAuth }));
2001
+ const startsNow = workerCanStart({ agent: task.agent, auth: workerAuth })
2002
+ && nextRunnableTasks(eligible, runningGoalIds, slow.enabled ? 1 : slots).some((candidate) => candidate.id === task.id);
1985
2003
  goal.startedAt = startsNow ? new Date().toISOString() : null;
1986
2004
  saveGoal(goal);
1987
2005
  }
@@ -4913,7 +4931,10 @@ server.listen(PORT, '127.0.0.1', () => {
4913
4931
  spawn('open', ['-g', openUrl], { stdio: 'ignore' }).unref();
4914
4932
  }
4915
4933
  console.log(`[manager] projects: ${projects.map((p) => `${p.id}=${p.dir}`).join(' ')}`);
4916
- probeWorkerAuth().catch(() => {}); // hold the queue + banner if `claude` isn't signed in
4934
+ probeWorkerAuth().catch(() => {}); // hold Claude tasks + banner if Claude is unavailable
4935
+ setInterval(() => {
4936
+ if (!workerAuth.ok) probeWorkerAuth().catch(() => {});
4937
+ }, 5 * 60_000).unref();
4917
4938
  syncAllExternal().catch(() => {});
4918
4939
  syncGoalMerges().catch(() => {});
4919
4940
  setInterval(() => {
@@ -4930,6 +4951,7 @@ server.listen(PORT, '127.0.0.1', () => {
4930
4951
  for (const p of projects) {
4931
4952
  const pending = tasks.filter((t) => t.projectId === p.id && t.status === 'queued').sort((a, b) => a.num - b.num);
4932
4953
  const q = queues.get(p.id);
4954
+ reconcileQueuedGoalStarts(p.id);
4933
4955
  q.waiting.push(...pending);
4934
4956
  if (pending.length) console.log(`[manager] ${p.id}: re-queued ${pending.length} pending task(s) after restart`);
4935
4957
  pump(p.id);
@@ -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.52",
3
+ "version": "0.10.54",
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": {