@galda/cli 0.10.115 → 0.10.117

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/CLAUDE.md CHANGED
@@ -5,7 +5,9 @@
5
5
  ## テスト(Masa指示 2026-07-03・必須)
6
6
 
7
7
  - `毎回、実装したものにはテストを書いて、実行する`。
8
- - engine/ の変更 → `node --test --test-concurrency=1 'engine/test/*.test.mjs'` が緑になるまで完了と言わない。
8
+ - engine/ の変更 → **`npm test`**(=`node --test --test-concurrency=1 --test-global-setup=engine/test/helpers/browser-setup.mjs 'engine/test/*.test.mjs'`)が緑になるまで完了と言わない。
9
+ - 🔴 **`--test-global-setup=…/browser-setup.mjs` を外さない**(2026-07-31 実測)。38ファイルがChromeを使う。付けると**スイート全体でChromeが1つ**になり、交互A/B実測で **825秒→231秒 / 416秒→261秒**、赤も **2件→1件**(ONで新しく壊れたテストはゼロ)。**外した時の実行時間は825秒と416秒で倍ぶれる**=「テストが遅い」ではなく「マシンの機嫌を測っている」状態に戻る。素の `node --test` を手打ちすると付き忘れるので `npm test` を使う。
10
+ - 🔴 **共有ブラウザを `close()` しない**。`getBrowser()` が返すのは**スイート全員のChrome**。閉じると以降の全ファイルが `connect ECONNREFUSED` で落ちる(2026-07-31 に実際にそうなっていて、この仕組みは実装済みなのに一度も動いていなかった)。終わりは必ず **`closeBrowser()`**=自分で起動した時だけ殺し、共有なら切断するだけ。自前で `puppeteer.connect` する場合も同じ(`later-list-new-ui.test.mjs` が手本)。
9
11
  - 🔴 **`--test-concurrency=1` は必須**(Masa報告のカオス調査で発見・2026-07-16)。既定(CPUコア数)だと
10
12
  58ファイルが同時に実サーバと Chrome を立てて**マシンを飽和させ、同じコードで赤が毎回入れ替わる**。
11
13
  **「テスト緑」がコードの状態でなく運を報告している状態を放置しない**=規律の土台。
package/engine/lib.mjs CHANGED
@@ -5355,6 +5355,67 @@ export function shouldEscalateConnectGateHint(waitedMs, email, thresholdMs = 150
5355
5355
  // for this" signal is a concrete MANAGER_PORT: the launcher (bin/manager-for-ai
5356
5356
  // .mjs) resolves a real port BEFORE spawn, while every ephemeral spawn uses
5357
5357
  // MANAGER_PORT=0 (OS-assigned). So port 0 is never a browser-opening boot.
5358
+ // What the terminal says when Galda starts.
5359
+ //
5360
+ // It used to say one log line among several — `[manager] Manager for AI →
5361
+ // http://localhost:4400/?key=…` — and then open a tab in the background, in
5362
+ // whatever browser the machine calls default. Masa's friend never saw that tab
5363
+ // (2026-07-31): it opened behind their windows in a browser they do not use, and
5364
+ // nothing on screen told them where their own copy of Galda was. The address is
5365
+ // the one thing a person needs, so it gets its own line and an instruction, and
5366
+ // the auto-opened tab is described as what it is — a convenience that may have
5367
+ // missed.
5368
+ //
5369
+ // Three destinations, and the wording differs because the links differ:
5370
+ // local — http://localhost:PORT/?key=… safe to paste anywhere on this Mac,
5371
+ // in any browser, as often as you like.
5372
+ // app — the hosted app; already signed in.
5373
+ // signin — a ONE-TIME sign-in link. Which browser it is opened in decides
5374
+ // which browser ends up signed in, so that choice is stated before
5375
+ // it is made rather than explained afterwards in a 400 page.
5376
+ // WHERE this install's Galda actually is. Decided from how it was installed, not
5377
+ // from whether a browser could be opened: `npx @galda/cli` configures the billing
5378
+ // + app URLs and those people live in the hosted app, while the localhost ?key
5379
+ // board is the entry only for local/dev. Reading this off the auto-open branch
5380
+ // (as the first version of the banner did) told a hosted user on Linux — or
5381
+ // anyone with MANAGER_OPEN_BROWSER=0 — to open a localhost board with an access
5382
+ // key, which the hosted flow has always refused to do.
5383
+ export function bootDestination({ hosted, signedIn, localUrl, appUrl, signinUrl } = {}) {
5384
+ if (!hosted) return { url: String(localUrl ?? ''), mode: 'local' };
5385
+ return signedIn
5386
+ ? { url: String(appUrl ?? ''), mode: 'app' }
5387
+ : { url: String(signinUrl ?? ''), mode: 'signin' };
5388
+ }
5389
+
5390
+ export function bootBanner({ url, mode = 'local', opened = false } = {}) {
5391
+ const address = String(url ?? '');
5392
+ if (!address) return [];
5393
+ const head = {
5394
+ local: 'Galda is running. Open this in your browser:',
5395
+ app: 'Galda is running. Open your board:',
5396
+ signin: 'Galda is running. Sign in to continue — open this in the browser you want to use Galda in:',
5397
+ }[mode] ?? 'Galda is running. Open this in your browser:';
5398
+ const foot = {
5399
+ local: 'Paste it into any browser on this Mac — the key is part of the address.',
5400
+ app: 'Paste it into any browser you are signed in to.',
5401
+ signin: 'This link works once. Whichever browser opens it is the one that ends up signed in.',
5402
+ }[mode];
5403
+ return [
5404
+ '',
5405
+ ` ${head}`,
5406
+ '',
5407
+ ` ${address}`,
5408
+ '',
5409
+ ` ${foot}`,
5410
+ // Said even when we opened a tab — especially then, for a sign-in link:
5411
+ // the tab we opened is what chose the browser, and the person may not have
5412
+ // seen it happen.
5413
+ ...(opened ? [' (A tab may already have opened in your default browser. If you did not see it,'
5414
+ + ' or you use a different browser, use the address above.)'] : []),
5415
+ '',
5416
+ ];
5417
+ }
5418
+
5358
5419
  export function shouldOpenBrowserOnBoot({ openBrowser, platform, managerPort } = {}) {
5359
5420
  if (String(openBrowser) !== '1') return false;
5360
5421
  if (platform !== 'darwin') return false;
package/engine/server.mjs CHANGED
@@ -20,7 +20,7 @@ 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 { folderLabel, chooseFolderScript, parseChosenFolder, buildFolderQuestion, resolveFolderAnswer, classifyFolderTarget, buildFolderInitConfirm, isFolderInitConfirmed, needsReviewPreference, buildReviewPreferenceQuestion, resolveReviewPreferenceAnswer, routeQuestionAnswer, notUnderstoodNote, parseRelocalizedQuestion, classifyAuthProbe, authBannerText, makeSigninChallenge, parseClaimResponse, isCommandOnPath, hostReadiness, parseRequestedWorkspaceDir } from './lib.mjs';
23
+ import { bootBanner, bootDestination, 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
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';
@@ -6304,7 +6304,25 @@ server.listen(PORT, '127.0.0.1', () => {
6304
6304
  }
6305
6305
  PORT = server.address().port; // the OS-assigned one when MANAGER_PORT=0
6306
6306
  try { server6.listen(PORT, '::1'); } catch { /* IPv6 loopback unavailable */ }
6307
- console.log(`[manager] Manager for AI → http://localhost:${PORT}/?key=${ACCESS_KEY}`);
6307
+ const localUrl = `http://localhost:${PORT}/?key=${ACCESS_KEY}`;
6308
+ // WHERE this person's Galda is depends on how it was installed, not on whether
6309
+ // we managed to open a tab. `npx @galda/cli` sets the billing + app URLs, and
6310
+ // those people live in the hosted app; the localhost ?key board is the entry
6311
+ // only for local/dev (MANAGER_BILLING_API_URL= opt-out, or `npm start` in the
6312
+ // repo). Deciding this from the auto-open branch — as this first did — told a
6313
+ // hosted user on Linux, or anyone with MANAGER_OPEN_BROWSER=0, to open a
6314
+ // localhost board with an access key: the one thing the flow below has always
6315
+ // refused to do.
6316
+ const hostedFlow = Boolean(BILLING_API_URL && APP_URL);
6317
+ const alreadySignedIn = existsSync(licenseFile) && !FORCE_SIGNIN;
6318
+ // buildSigninUrl() also starts the background claim poll — which is exactly
6319
+ // what a person about to open that link by hand needs running, so it is called
6320
+ // only on the branch that will actually show it.
6321
+ const { url: bannerUrl, mode: bannerMode } = bootDestination({
6322
+ hosted: hostedFlow, signedIn: alreadySignedIn, localUrl, appUrl: APP_URL,
6323
+ signinUrl: hostedFlow && !alreadySignedIn ? buildSigninUrl() : '',
6324
+ });
6325
+ let bannerOpened = false;
6308
6326
  if (shouldOpenBrowserOnBoot({ openBrowser: process.env.MANAGER_OPEN_BROWSER, platform: process.platform, managerPort: process.env.MANAGER_PORT })) {
6309
6327
  // Hosted flow (billing worker + named app configured): the user lives in
6310
6328
  // app.galda.app, driven over the relay — they must NEVER be dropped on a
@@ -6313,18 +6331,10 @@ server.listen(PORT, '127.0.0.1', () => {
6313
6331
  // → one-click Google sign-in; already signed in → straight to the app (the
6314
6332
  // board comes alive as relay-client rebinds via its license.token watcher).
6315
6333
  // The localhost ?key board stays the entry ONLY for local/dev (no billing).
6316
- let openUrl = `http://localhost:${PORT}/?key=${ACCESS_KEY}`;
6317
- if (BILLING_API_URL && APP_URL) {
6318
- const signedIn = existsSync(licenseFile) && !FORCE_SIGNIN;
6319
- openUrl = signedIn
6320
- ? APP_URL
6321
- : buildSigninUrl(); // device-flow: also starts the background claim poll
6322
- console.log(signedIn
6323
- ? `[manager] opening your app → ${APP_URL}`
6324
- : `[manager] ${FORCE_SIGNIN ? 'switching account' : 'first run'}: opening one-click sign-in → ${openUrl}`);
6325
- }
6326
- spawn('open', ['-g', openUrl], { stdio: 'ignore' }).unref();
6334
+ bannerOpened = true;
6335
+ spawn('open', ['-g', bannerUrl], { stdio: 'ignore' }).unref();
6327
6336
  }
6337
+ for (const line of bootBanner({ url: bannerUrl, mode: bannerMode, opened: bannerOpened })) console.log(line);
6328
6338
  console.log(`[manager] projects: ${projects.map((p) => `${p.id}=${p.dir}`).join(' ')}`);
6329
6339
  if (shouldProbeClaudeAuth(AVAILABLE_AGENTS)) probeWorkerAuth().catch(() => {}); // hold Claude tasks + banner if Claude is unavailable
6330
6340
  setInterval(() => {
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@galda/cli",
3
- "version": "0.10.115",
3
+ "version": "0.10.117",
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": {
7
7
  "start": "node engine/server.mjs",
8
- "test": "node --test --test-concurrency=1 'engine/test/*.test.mjs'"
8
+ "test": "node --test --test-concurrency=1 --test-global-setup=engine/test/helpers/browser-setup.mjs 'engine/test/*.test.mjs'"
9
9
  },
10
10
  "dependencies": {
11
11
  "puppeteer-core": "^23.11.1",