@galda/cli 0.10.11 → 0.10.13

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
@@ -5814,12 +5814,19 @@ function bootDisconnected(info){
5814
5814
  const cw = document.getElementById('composerWrap');
5815
5815
  if (cw) cw.addEventListener('mousedown', (e) => { if (e.target.closest('#appsel, #modelsel, #effortsel')) return; gate(e); });
5816
5816
 
5817
- // come alive automatically: when the relay stops flagging disconnected (the
5818
- // engine connected its live app is proxied), reload into the real board.
5817
+ // Come alive automatically: while this device's engine is offline the relay
5818
+ // 503s every engine-backed path (only /, /theme.css, /fonts.css are served);
5819
+ // the instant the agent connects, the same path is proxied to the live engine
5820
+ // (200). So we probe an engine-only endpoint by STATUS and reload when it stops
5821
+ // 503-ing. We must NOT decide this by fetching `/` and grepping its HTML for a
5822
+ // sentinel: this very poll's source contains the string '__GALDA_DISCONNECTED',
5823
+ // so it is baked into every proxied copy of the page — the match is ALWAYS
5824
+ // "found", the reload never fires, and the tab stays stuck on the connect
5825
+ // screen forever ("npx said done but nothing happens"). (fix: status, not body)
5819
5826
  setInterval(() => {
5820
- fetch('/', { cache: 'no-store', credentials: 'same-origin' }).then((r) => r.text()).then((t) => {
5821
- if (t.indexOf('__GALDA_DISCONNECTED') === -1) location.reload();
5822
- }).catch(() => {});
5827
+ fetch('/api/state', { cache: 'no-store', credentials: 'same-origin' })
5828
+ .then((r) => { if (r.status !== 503) location.reload(); })
5829
+ .catch(() => {});
5823
5830
  }, 4000);
5824
5831
  }
5825
5832
 
@@ -100,8 +100,19 @@ function connect(retryMs = 1000) {
100
100
  live.delete(m.id);
101
101
  }
102
102
  };
103
- ws.onclose = () => {
103
+ ws.onclose = (ev) => {
104
104
  if (ws !== currentWs) return; // a superseded socket (we already moved on) — don't reconnect
105
+ // Close 4000 = the relay evicted us because a NEWER agent for the SAME account
106
+ // connected (server.mjs "replaced by a newer connection"). If we reconnect we
107
+ // evict THAT one → the two instances mutual-kick forever, which surfaces to the
108
+ // user as app.galda.app reload-looping. Stand down: the newest instance is
109
+ // authoritative. (A normal network drop uses a different code and still
110
+ // reconnects; sign-in via the watcher below can still retake.)
111
+ if (ev && ev.code === 4000) {
112
+ console.log('[agent] relay: superseded by a newer instance of this account — standing down (run only one manager per account)');
113
+ currentWs = null;
114
+ return;
115
+ }
105
116
  console.log(`[agent] relay connection lost — retrying in ${retryMs}ms`);
106
117
  scheduleReconnect(retryMs);
107
118
  };
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, buildFolderQuestion, resolveFolderAnswer } from './lib.mjs';
24
- import { parseStreamEvents, parsePlan, refinePlanTasks, findOverlappingGoal, canEditGoal, canDeleteGoal, shouldAskClarification, workerExitReason, isForcedStop, workerResultText, taskCountsAsComplete, resolveWantsPR, resolveGoalSource, buildGoalSummary, buildGoalProofMd, buildReviewRuleSection, nextGoalStatus, isPrMerged, isPrApproved, reviewToDoneStatus, advanceReviewGoal, revertReviewGoal, approveGoal, dismissGoal, 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, buildEphemeralSeedLog, buildEphemeralSeedProjects, trimTaskActivityForState, buildAskContext, WORKER_TOOLS, verifyCfAccessJwt, resolveIdentity, goalVisibleTo, clampParallelLimit, canStartMore, nextRunnableTasks, goalsConflict, detectConflicts, isUiChange, shouldCaptureProof, shouldCaptureBaseline, hasTestRelevantChanges, buildExecutionPlan, buildContextHandoffSummary, buildFailurePostmortem, appendFailureMemory, latestFailurePolicy, classifyChangeRisk, checkRunBudget, usageBudgetTokens, isRateLimited, nextResumeDelay, checkFreeTierLimit, resolveEntitlement, resolveCachedEntitlement, FREE_TIER_LIMITS, verifyLicenseToken, shouldEmitSetupCompleted, pickAnalyticsUid, shouldEmitFreeExhausted, detectRequestLanguage, isNothingVerifiable } from './lib.mjs';
23
+ import { needsProjectFolder, folderLabel, buildFolderQuestion, resolveFolderAnswer, needsReviewPreference, buildReviewPreferenceQuestion, resolveReviewPreferenceAnswer } from './lib.mjs';
24
+ import { parseStreamEvents, parsePlan, refinePlanTasks, findOverlappingGoal, canEditGoal, canDeleteGoal, shouldAskClarification, workerExitReason, isForcedStop, workerResultText, taskCountsAsComplete, resolveWantsPR, resolveGoalSource, buildGoalSummary, buildGoalProofMd, buildGoalPrBody, nextGoalStatus, isPrMerged, isPrApproved, reviewToDoneStatus, advanceReviewGoal, revertReviewGoal, approveGoal, dismissGoal, 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, buildEphemeralSeedLog, buildEphemeralSeedProjects, trimTaskActivityForState, buildAskContext, WORKER_TOOLS, verifyCfAccessJwt, resolveIdentity, goalVisibleTo, clampParallelLimit, canStartMore, nextRunnableTasks, goalsConflict, detectConflicts, isUiChange, shouldCaptureProof, shouldCaptureBaseline, hasTestRelevantChanges, buildExecutionPlan, buildContextHandoffSummary, buildFailurePostmortem, appendFailureMemory, latestFailurePolicy, classifyChangeRisk, checkRunBudget, usageBudgetTokens, isRateLimited, nextResumeDelay, checkFreeTierLimit, resolveEntitlement, resolveCachedEntitlement, FREE_TIER_LIMITS, verifyLicenseToken, shouldEmitSetupCompleted, pickAnalyticsUid, shouldEmitFreeExhausted, detectRequestLanguage, isNothingVerifiable } from './lib.mjs';
25
25
  import { openPR } from './pr.mjs';
26
26
  import { runVerification, exerciseUi } from './verify.mjs';
27
27
 
@@ -2160,6 +2160,11 @@ async function finishGoalIfComplete(goal, project, activityTask = null) {
2160
2160
  // point (all its tasks are done), so its conflictsWith is accurate even if
2161
2161
  // PR creation itself fails (e.g. no `gh` auth) or is still no-op'd (no code).
2162
2162
  updateGoalConflicts(goal);
2163
+ // Computed BEFORE createGoalPR (not inside verifyGate, which used to run
2164
+ // after) so the PR body's "確認ポイント" headline is available the first
2165
+ // time the PR is opened, not only on the review card (Masa 2026-07-16).
2166
+ goalAct('Writing the review summary from the worker report and diff.');
2167
+ goal.reviewSummary = await summarizeForReview(goal, siblings, wdir, reviewDefinition);
2163
2168
  goalAct('Checking changed files and preparing the PR branch if code changed.');
2164
2169
  await createGoalPR(goal, prProject, siblings, reviewDefinition, activityTask);
2165
2170
  // goal 427 §4: classify the change's risk from the SAME changed-files/diff
@@ -2228,8 +2233,13 @@ async function verifyGate(goal, prProject, siblings, baseProject, activityTask =
2228
2233
  goal.testResult = { ran: false, skipped: true, skippedReason: changedFiles.length ? 'documentation-only change' : 'no changed files' };
2229
2234
  goalAct(`Skipped project tests: ${goal.testResult.skippedReason}.`);
2230
2235
  }
2231
- goalAct('Writing the review summary from the worker report and diff.');
2232
- goal.reviewSummary = await summarizeForReview(goal, siblings, wdir, reviewDefinition);
2236
+ // Normally already set by finishGoalIfComplete before the PR was opened
2237
+ // (so the PR body can include it too); only fall back here for the
2238
+ // standalone /reverify path or any older goal that never got one.
2239
+ if (!goal.reviewSummary) {
2240
+ goalAct('Writing the review summary from the worker report and diff.');
2241
+ goal.reviewSummary = await summarizeForReview(goal, siblings, wdir, reviewDefinition);
2242
+ }
2233
2243
  const uiTasks = siblings.filter((t) => isUiChange(t.changedFiles));
2234
2244
  // Only require/capture proof for projects we can actually boot & screenshot
2235
2245
  // (Manager-shaped). A generic web repo that edits a stylesheet can't be
@@ -2371,26 +2381,12 @@ async function createGoalPR(goal, project, goalTasks, reviewDefinition = DEFAULT
2371
2381
  // (Masa: 「ユーザーの言語で」).
2372
2382
  const lang = reviewDefinition.language === 'auto' ? detectRequestLanguage(goal.text) : (reviewDefinition.language || 'ja');
2373
2383
  const L = (en, ja) => (lang === 'en' ? en : ja);
2374
- const bodyFor = (branch) => [
2375
- ...(proofItems.length && owner ? [
2376
- L('## Proof — one tap, plays in place', '## 動作確認 — 1タップでその場で再生'),
2377
- '',
2378
- `https://github.com/${owner}/blob/${branch}/${proofRel}/PROOF.md`,
2379
- '',
2380
- ] : []),
2381
- L('## Goal', '## ゴール'),
2382
- '',
2383
- goal.text.slice(0, 1500),
2384
- '',
2385
- ...buildReviewRuleSection(reviewDefinition.description),
2386
- L(`Implemented by Manager for AI: the goal was decomposed into ${goalTasks.length} task(s), each executed by a Claude Code worker (flat-rate, no extra API), tests written and run per task.${proofItems.length ? ' Tasks with a pass condition were verified independently in a headless browser.' : ''}`,
2387
- `Manager for AI が実装: ゴールを${goalTasks.length}個のタスクに分解し、Claude Code worker が順に実行(既存サブスク・追加API無し・タスク毎にテストを作成し実行)。${proofItems.length ? '成功条件つきタスクは headless ブラウザで独立検証済み。' : ''}`),
2388
- '',
2389
- L('## Tasks and worker reports', '## タスクとworker報告'),
2390
- ...goalTasks.map((t) => `\n### ${t.num} ${t.title} — ${t.status} (${t.secs}s)\n${(t.result ?? '').slice(0, 800)}`),
2391
- '',
2392
- L(`Changed files: ${files.map((f) => `\`${f}\``).join(' ')}`, `変更ファイル: ${files.map((f) => `\`${f}\``).join(' ')}`),
2393
- ].join('\n');
2384
+ const bodyFor = (branch) => buildGoalPrBody({
2385
+ lang, goal, goalTasks, files, owner, branch, proofRel,
2386
+ proofItemsCount: proofItems.length,
2387
+ reviewDescription: reviewDefinition.description,
2388
+ reviewSummary: goal.reviewSummary,
2389
+ });
2394
2390
  const title = `Manager for AI: ${isRework ? L('rework — ', '再作業 — ') : ''}${goal.plan?.[0] ?? goal.text.slice(0, 60)}${goal.plan?.length > 1 ? L(` +${goal.plan.length - 1} more`, ` 他${goal.plan.length - 1}件`) : ''}`;
2395
2391
  const { url, branch } = openPR({
2396
2392
  repoDir: project.dir, slug: `goal-${goal.id}`,
@@ -2401,7 +2397,7 @@ async function createGoalPR(goal, project, goalTasks, reviewDefinition = DEFAULT
2401
2397
  extraCopies,
2402
2398
  extraWrites: proofItems.length ? [{
2403
2399
  destRel: `${proofRel}/PROOF.md`,
2404
- content: buildGoalProofMd({ goalText: goal.text, items: proofItems }),
2400
+ content: buildGoalProofMd({ goalText: goal.text, items: proofItems, lang }),
2405
2401
  }] : [],
2406
2402
  ...(isRework ? { existingBranch: goal.prBranch, existingPrUrl: goal.pr } : {}),
2407
2403
  });
@@ -2750,15 +2746,22 @@ const server = createServer(async (req, res) => {
2750
2746
  // for pending/review items (they never spawn a worker).
2751
2747
  const runnable = !pending && !review;
2752
2748
  const needsFolder = runnable && needsProjectFolder({ dir: project.dir, homeDir: HOME, isRepo: isGitRepo(project.dir) });
2753
- const folderQuestion = needsFolder
2754
- ? buildFolderQuestion(detectRepos(HOME), { lang: /[぀-ヿ㐀-鿿]/.test(text) ? 'ja' : 'en' })
2755
- : undefined;
2749
+ const lang = /[぀-ヿ㐀-鿿]/.test(text) ? 'ja' : 'en';
2750
+ const folderQuestion = needsFolder ? buildFolderQuestion(detectRepos(HOME), { lang }) : undefined;
2751
+ // Task 5 (PR運用ヒアリング): once the folder is settled, ask — once per
2752
+ // project, before its first runnable goal — whether this project wants
2753
+ // PRs. Skipped this round if a folder question already claimed the
2754
+ // needsInput slot; that goal gets rebound to a real repo via /answer
2755
+ // and the NEXT goal for that project is what actually asks this.
2756
+ const needsReviewPref = runnable && !needsFolder && needsReviewPreference({ askedBefore: project.reviewPrefAsked });
2757
+ const reviewPrefQuestion = needsReviewPref ? buildReviewPreferenceQuestion({ lang }) : undefined;
2758
+ if (needsReviewPref) { project.reviewPrefAsked = true; saveProjects(); }
2756
2759
  const goalAgent = workerAgent(agent);
2757
2760
  const priorFailureMemory = recentProjectFailureMemory(projectId, text.trim());
2758
2761
  const goal = {
2759
2762
  id: nextId++, projectId, text: text.trim().slice(0, 8000),
2760
- status: needsFolder ? 'needsInput' : review ? 'review' : pending ? 'pending' : 'planning',
2761
- question: folderQuestion,
2763
+ status: (needsFolder || needsReviewPref) ? 'needsInput' : review ? 'review' : pending ? 'pending' : 'planning',
2764
+ question: folderQuestion || reviewPrefQuestion,
2762
2765
  reviewOnly: review ? true : undefined,
2763
2766
  note: review && typeof note === 'string' && note.trim() ? note.trim().slice(0, 8000) : undefined,
2764
2767
  wantsPR: review ? false : resolveWantsPR(pr, text, getReviewDefinition(projectId).defaultWantsPR),
@@ -2788,7 +2791,7 @@ const server = createServer(async (req, res) => {
2788
2791
  // pending = deliberately shelved (Phase 2 material): never planned,
2789
2792
  // never queued, until POST /api/goals/:id/activate. review-only items
2790
2793
  // are terminal-until-human too, so they also skip planGoal.
2791
- if (runnable && !needsFolder) {
2794
+ if (runnable && !needsFolder && !needsReviewPref) {
2792
2795
  planGoal(goal).catch((e) => {
2793
2796
  goal.status = 'failed'; goal.prError = String(e).slice(0, 300); saveGoal(goal);
2794
2797
  });
@@ -3115,6 +3118,28 @@ const server = createServer(async (req, res) => {
3115
3118
  saveGoal(goal);
3116
3119
  return json(res, 200, goal);
3117
3120
  }
3121
+ // Review-preference answer (task 5 onboarding funnel): fold into the
3122
+ // project's review definition (defaultWantsPR) rather than goal.text —
3123
+ // like the folder question, this is project setup, not a task detail.
3124
+ if (goal.question?.kind === 'reviewPref') {
3125
+ const project = projects.find((p) => p.id === goal.projectId);
3126
+ const patch = resolveReviewPreferenceAnswer(answer, goal.question);
3127
+ if (project) {
3128
+ const merged = validateReviewDefinition({ ...getReviewDefinition(project.id), ...patch });
3129
+ if (merged.ok) {
3130
+ reviewDefinitions[project.id] = merged.definition;
3131
+ saveReviewDefinitions();
3132
+ send({ ev: 'review-definition', projectId: project.id, reviewDefinition: merged.definition });
3133
+ }
3134
+ }
3135
+ goal.wantsPR = resolveWantsPR(undefined, goal.text, patch.defaultWantsPR);
3136
+ goal.question = null;
3137
+ goal.clarified = true;
3138
+ goal.status = 'planning';
3139
+ saveGoal(goal);
3140
+ planGoal(goal).catch((e) => { goal.status = 'failed'; goal.prError = String(e).slice(0, 300); saveGoal(goal); });
3141
+ return json(res, 200, goal);
3142
+ }
3118
3143
  goal.text = `${goal.text}\n[確認: ${goal.question?.text ?? ''} → ${answer}]`;
3119
3144
  goal.clarified = true; // answered once → planGoal must never re-ask (persisted via saveGoal)
3120
3145
  goal.question = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@galda/cli",
3
- "version": "0.10.11",
3
+ "version": "0.10.13",
4
4
  "type": "module",
5
5
  "description": "Galda - hand off work to your Claude Code, get proof back. Runs on your existing subscription, no extra API cost.",
6
6
  "scripts": {