@galda/cli 0.10.106 → 0.10.108

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/engine/lib.mjs CHANGED
@@ -1896,6 +1896,51 @@ export function canStartMore(runningCount, limit) {
1896
1896
  return Number.isFinite(limit) && limit > 0 && (runningCount ?? 0) < limit;
1897
1897
  }
1898
1898
 
1899
+ // A goal sitting in `stacked` is not being worked on and did not fail — it is
1900
+ // waiting. Say why, and say what has to happen for it to start, because "nothing
1901
+ // is happening" is indistinguishable from "it is broken" from the outside.
1902
+ //
1903
+ // Keyed by GOAL id, not task id: a stacked goal has no task yet. That is the whole
1904
+ // point of the slot accounting (#541) — a goal holds its slot for as long as it is
1905
+ // in Doing, so the ones beyond the cap stay `stacked` instead of quietly planning
1906
+ // and making the board claim four things are being worked on when two are.
1907
+ //
1908
+ // The implementation of this shipped once (goal 058a11c, 2026-07-26) and was lost
1909
+ // when PR #396 integrated the stability roadmap: the test came across, the server
1910
+ // and app halves did not. main has carried a red test for it ever since.
1911
+ export function buildQueueWaits({ goals = [], projectPaused = () => false, projectCap = 2, globalCap = 4 } = {}) {
1912
+ const waits = {};
1913
+ const isActive = (g) => g.status === 'planning' || g.status === 'running';
1914
+ const activeEverywhere = goals.filter(isActive);
1915
+ for (const goal of goals) {
1916
+ if (goal?.status !== 'stacked') continue;
1917
+ const active = activeEverywhere.filter((g) => g.projectId === goal.projectId);
1918
+ if (projectPaused(goal.projectId)) {
1919
+ waits[goal.id] = {
1920
+ reason: 'this project is paused',
1921
+ condition: 'you resume the project',
1922
+ targetGoalIds: [],
1923
+ };
1924
+ } else if (active.length < projectCap && activeEverywhere.length >= globalCap) {
1925
+ // This project has room; the machine does not. Naming the other projects'
1926
+ // goals here would point at work the person may not even be looking at, so
1927
+ // the target stays the global limit itself.
1928
+ waits[goal.id] = {
1929
+ reason: `all ${globalCap} slots across every project are already Doing`,
1930
+ condition: 'one of them finishes, fails, or moves to Review',
1931
+ targetGoalIds: activeEverywhere.map((g) => g.id),
1932
+ };
1933
+ } else {
1934
+ waits[goal.id] = {
1935
+ reason: `all ${projectCap} project slots are already Doing`,
1936
+ condition: 'one of them finishes, fails, or moves to Review',
1937
+ targetGoalIds: active.map((g) => g.id),
1938
+ };
1939
+ }
1940
+ }
1941
+ return waits;
1942
+ }
1943
+
1899
1944
  // Pick up to `limit` tasks from the front of `waiting` that are safe to start
1900
1945
  // RIGHT NOW, preserving same-goal ordering: a goal already represented in
1901
1946
  // `runningGoalIds` (has an in-flight task) contributes nothing this round,
@@ -3609,6 +3654,44 @@ export function pickChromePath(env = {}, exists = () => false) {
3609
3654
  return chromeCandidates(env).find((c) => exists(c)) ?? null;
3610
3655
  }
3611
3656
 
3657
+ // What THIS machine can and cannot do, stated before the person spends an hour
3658
+ // reading the absence as a product bug. Two capabilities are host-dependent and
3659
+ // silently degrade today: without Chrome nothing can take a screenshot or verify
3660
+ // in a browser, and without a native picker "Open folder…" cannot open Finder.
3661
+ //
3662
+ // Every check here is ADVISORY. None of them stop anything — the app works with
3663
+ // neither. That is deliberately NOT testEnvironmentPreflight's blocking/canRun
3664
+ // model (which answers "can `npm test` even start"): mixing the two is what made
3665
+ // a missing puppeteer read as a failed verification.
3666
+ // (`existsFn`/`env` are injected so this stays a pure function under test.)
3667
+ export function hostReadiness({ platform = 'darwin', env = {}, existsFn = () => false } = {}) {
3668
+ const chrome = pickChromePath(env, existsFn);
3669
+ const onPath = (cmd) => isCommandOnPath(cmd, {
3670
+ pathEnv: env.PATH ?? '', sep: platform === 'win32' ? ';' : ':', pathExt: env.PATHEXT ?? '', existsFn,
3671
+ });
3672
+ // macOS and Windows ship theirs; Linux has two common ones and neither is a given.
3673
+ const picker = platform === 'darwin' ? 'osascript'
3674
+ : platform === 'win32' ? 'powershell.exe'
3675
+ : (onPath('zenity') ? 'zenity' : 'kdialog');
3676
+ const hasPicker = onPath(picker);
3677
+ return [
3678
+ {
3679
+ id: 'chrome',
3680
+ ok: Boolean(chrome),
3681
+ detail: chrome
3682
+ ? `Chrome/Chromium: ${chrome}`
3683
+ : 'No Chrome/Chromium found — screenshots and browser checks are unavailable',
3684
+ },
3685
+ {
3686
+ id: 'folder-picker',
3687
+ ok: hasPicker,
3688
+ detail: hasPicker
3689
+ ? `Native folder picker: ${picker}`
3690
+ : `No native folder picker (${picker} is not installed) — Open folder… asks you to type the path`,
3691
+ },
3692
+ ];
3693
+ }
3694
+
3612
3695
  // `node <manager>/engine/shot.mjs <url|file> <out.png> [--width N] [--height N]
3613
3696
  // [--full] [--wait MS] [--wait-for SELECTOR] [--element SELECTOR] [--clip x,y,w,h]`
3614
3697
  // Returns { error } instead of throwing, so the CLI can print one clear line.
package/engine/server.mjs CHANGED
@@ -20,8 +20,8 @@ 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, isCommandOnPath, parseRequestedWorkspaceDir } from './lib.mjs';
24
- import { parseStreamEvents, parseCodexEvents, buildCodexArgs, parsePlan, refinePlanTasks, canEditGoal, canDeleteGoal, shouldAskClarification, workerExitReason, classifyWorkerFailure, isForcedStop, taskStatusAfterVerify, workerResultText, taskCountsAsComplete, resolveWantsPR, resolveGoalSource, buildGoalSummary, buildReviewAttemptLedger, latestGoalOutcomeTask, buildDirtyWorkspacePrBlock, 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, rebasePreviewUrl, shouldCreateGoalPR, normalizePullRequestUrl, 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, autoFoldReviewsEnabled, parseGitUnifiedDiffLocations, hasTestRelevantChanges, parseFailingTestNames, classifyTestGate, classifyPrSafety, isInconclusiveTestRun, countInfraFlakes, classifyRunFailures, classifyVerifyGate, buildVerificationInfraError, verificationInfraErrorFrom, 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, testFailureReason, manualTestRetryPrompt, isNothingVerifiable, isNothingVerifiableForPR, isSuspiciouslyIncompleteDone, classifyComposerIntentHeuristic, detectPauseIntent, buildIntentPrompt, parseIntentResponse, buildBoardSnapshot, validateBoardSnapshot, boardIsEmpty, decideBoardPull, resolveConnectedAgents, pickAvailableAgent, pickUtilityAgent, utilityModel, liveTakeoverDecision, shouldOpenBrowserOnBoot, goalTaskTitle, buildGoalTask, shouldUsePlannerForGoal, RUN_DECL_FILE, REQUIREMENT_EVIDENCE_FILE, parseRequirementEvidenceDocument, parseRunDeclaration, SHOT_DIR, collectShotNames, parseGoalAddress, REPLY_OUTCOMES, buildGoalMessage, serializeGoalMessage, parseGoalMessages, computeInternalQualityMetrics, classifyInternalQualityTaskError, resolveWorkspaceMode, goalSessionFor, setGoalAgentSession, switchGoalAgentSession, parseApprovalRequest } 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, hostReadiness, parseRequestedWorkspaceDir } from './lib.mjs';
24
+ import { parseStreamEvents, parseCodexEvents, buildCodexArgs, parsePlan, refinePlanTasks, canEditGoal, canDeleteGoal, shouldAskClarification, workerExitReason, classifyWorkerFailure, isForcedStop, taskStatusAfterVerify, workerResultText, taskCountsAsComplete, resolveWantsPR, resolveGoalSource, buildGoalSummary, buildReviewAttemptLedger, latestGoalOutcomeTask, buildDirtyWorkspacePrBlock, 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, rebasePreviewUrl, shouldCreateGoalPR, normalizePullRequestUrl, 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, buildQueueWaits, nextRunnableTasks, goalsConflict, detectConflicts, pickFoldTarget, autoFoldReviewsEnabled, parseGitUnifiedDiffLocations, hasTestRelevantChanges, parseFailingTestNames, classifyTestGate, classifyPrSafety, isInconclusiveTestRun, countInfraFlakes, classifyRunFailures, classifyVerifyGate, buildVerificationInfraError, verificationInfraErrorFrom, 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, testFailureReason, manualTestRetryPrompt, isNothingVerifiable, isNothingVerifiableForPR, isSuspiciouslyIncompleteDone, classifyComposerIntentHeuristic, detectPauseIntent, buildIntentPrompt, parseIntentResponse, buildBoardSnapshot, validateBoardSnapshot, boardIsEmpty, decideBoardPull, resolveConnectedAgents, pickAvailableAgent, pickUtilityAgent, utilityModel, liveTakeoverDecision, shouldOpenBrowserOnBoot, goalTaskTitle, buildGoalTask, shouldUsePlannerForGoal, RUN_DECL_FILE, REQUIREMENT_EVIDENCE_FILE, parseRequirementEvidenceDocument, parseRunDeclaration, SHOT_DIR, collectShotNames, parseGoalAddress, REPLY_OUTCOMES, buildGoalMessage, serializeGoalMessage, parseGoalMessages, computeInternalQualityMetrics, classifyInternalQualityTaskError, resolveWorkspaceMode, goalSessionFor, setGoalAgentSession, switchGoalAgentSession, parseApprovalRequest } from './lib.mjs';
25
25
  import { migrateRequirementModel, validateRequirementModel } from './requirement-model.mjs';
26
26
  import { buildManagerVerificationChecks, buildRequirementVerificationEvidence, mergeRequirementVerificationEvidence } from './requirement-verification.mjs';
27
27
  import { createSerialQueue } from './lib.mjs';
@@ -3335,6 +3335,14 @@ function testNodePath(dir, dependencySourceDir = null) {
3335
3335
  .filter(Boolean).filter(existsSync);
3336
3336
  return [...new Set([...local, ...(process.env.NODE_PATH ? process.env.NODE_PATH.split(sep) : [])])].join(sep);
3337
3337
  }
3338
+ // Advisory host capabilities for /api/state (see hostReadiness in lib.mjs). This
3339
+ // touches the filesystem, and /api/state is polled every couple of seconds, so
3340
+ // answer once: installing Chrome mid-session is not a thing to poll for, and a
3341
+ // restart is already how this app picks up a new environment.
3342
+ let _hostReadiness = null;
3343
+ function hostReadinessNow() {
3344
+ return (_hostReadiness ??= hostReadiness({ platform: process.platform, env: process.env, existsFn: existsSync }));
3345
+ }
3338
3346
  function testEnvironmentPreflight(dir, dependencySourceDir = null) {
3339
3347
  const nodePath = testNodePath(dir, dependencySourceDir);
3340
3348
  const sources = nodePath ? nodePath.split(process.platform === 'win32' ? ';' : ':') : [];
@@ -4367,7 +4375,7 @@ const server = createServer(async (req, res) => {
4367
4375
  const dirty = projectDirtyInfo(p.dir);
4368
4376
  return { ...p, ...(branch ? { branch } : {}), ...dirty };
4369
4377
  });
4370
- return json(res, 200, { projects: projectsWithBranch, goals: visibleGoals, tasks: trimTaskActivityForState(visibleTasks), models: MODELS, agentModels: { 'claude-code': MODELS, codex: CODEX_MODELS }, agentEfforts: { 'claude-code': CLAUDE_EFFORTS, codex: CODEX_EFFORTS }, agents: AVAILABLE_AGENTS, connectCommand, queueOrder, externalActivity, workflowColumns: projectColumns, reviewDefinitions: projectReviewDefs, runningCounts, parallelLimits, uiVersion: uiVersion(), billing, auth: publicAuth() });
4378
+ return json(res, 200, { projects: projectsWithBranch, goals: visibleGoals, tasks: trimTaskActivityForState(visibleTasks), models: MODELS, agentModels: { 'claude-code': MODELS, codex: CODEX_MODELS }, agentEfforts: { 'claude-code': CLAUDE_EFFORTS, codex: CODEX_EFFORTS }, agents: AVAILABLE_AGENTS, connectCommand, queueOrder, externalActivity, workflowColumns: projectColumns, reviewDefinitions: projectReviewDefs, runningCounts, parallelLimits, uiVersion: uiVersion(), billing, hostReadiness: hostReadinessNow(), queueWaits: buildQueueWaits({ goals: visibleGoals, projectPaused: isProjectPaused, projectCap: projectParallelCap(), globalCap: GLOBAL_MAX_PARALLEL }), auth: publicAuth() });
4371
4379
  }
4372
4380
 
4373
4381
  // Read-only stability snapshot: separates implementation failures from
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@galda/cli",
3
- "version": "0.10.106",
3
+ "version": "0.10.108",
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": {