@galda/cli 0.10.57 → 0.10.60

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
@@ -7055,9 +7055,10 @@ function renderFsTodo(){
7055
7055
  for (const b of el.querySelectorAll('[data-fslog]')) b.onclick = () => fsToggleLog(Number(b.dataset.fslog));
7056
7056
  for (const b of el.querySelectorAll('[data-fsglog]')) b.onclick = () => toggleGoalDetail(Number(b.dataset.fsglog));
7057
7057
  for (const b of el.querySelectorAll('[data-fsreverify]')) b.onclick = async () => {
7058
- b.disabled = true; b.textContent = 'Verifying…';
7058
+ b.disabled = true; b.textContent = 'Starting…';
7059
7059
  const r = await fetch(withKey(`/api/goals/${b.dataset.fsreverify}/reverify`), { method: 'POST' });
7060
- if (!r.ok) showErr('再検証に失敗'); else await refresh();
7060
+ if (!r.ok) { showErr(document.documentElement.lang === 'ja' ? 'Retryに失敗しました' : 'Retry failed.'); b.disabled = false; b.textContent = 'Retry'; }
7061
+ else await refresh();
7061
7062
  };
7062
7063
  for (const b of el.querySelectorAll('[data-fsarchive]')) b.onclick = async () => {
7063
7064
  const r = await fetch(withKey(`/api/goals/${b.dataset.fsarchive}/archive`), { method: 'POST' });
@@ -9452,6 +9453,11 @@ async function saEnhanceShotBoxes(){
9452
9453
  }
9453
9454
  function saRender(){
9454
9455
  const S = $('seeall'); if (!S) return;
9456
+ // SSE, diff fetches and action responses rebuild the dialog. Remember the
9457
+ // review body's position before replacing it so reading never jumps to top.
9458
+ const previousBody = S.querySelector('.sa-body');
9459
+ const previousScrollTop = previousBody?.scrollTop ?? 0;
9460
+ const restoreScroll = Boolean(previousBody) && SA.focusGid == null;
9455
9461
  const pending = SA.items.filter((x) => x.st === 0);
9456
9462
  const green = pending.filter((x) => !x.testResult || x.testResult.ok !== false).length;
9457
9463
  // Solo mode = a single "Needs you" goal opened as this card. It is not a review batch, so the
@@ -9464,6 +9470,16 @@ function saRender(){
9464
9470
  const hdKeys = solo ? '↑↓ MOVE · ESC CLOSE' : '↑↓ MOVE · A APPROVE · D DISMISS · ESC CLOSE';
9465
9471
  S.innerHTML = `<div class="sa-panel"><div class="sa-hd"><span class="t">${hdTitle}</span><span class="c">${c}</span><span class="sa-count">${posLabel}</span><span class="keys">${hdKeys}</span><button class="sa-x" data-saact="close:0">✕</button></div>
9466
9472
  <div class="sa-body">${SA.items.length ? SA.items.map((it, i) => saItemHtml(it, i)).join('') : '<div class="sa-empty">Nothing waiting for review.</div>'}</div></div>`;
9473
+ const body = S.querySelector('.sa-body');
9474
+ if (body && restoreScroll) body.scrollTop = previousScrollTop;
9475
+ // Once the person scrolls, their position wins over the short opening-focus
9476
+ // animation. Programmatic opening scroll fires this too, which is fine: the
9477
+ // first placement happened and later SSE ticks should preserve it.
9478
+ body?.addEventListener('scroll', () => {
9479
+ if (SA.focusGid == null) return;
9480
+ SA.focusGid = null;
9481
+ if (SA.focusTimer) { clearTimeout(SA.focusTimer); SA.focusTimer = null; }
9482
+ }, { once: true });
9467
9483
  // Native <details> would collapse on every rebuild (SSE tick, approve, etc.) —
9468
9484
  // track what's open per goal+section in SA.detOpen (persists across re-renders)
9469
9485
  // and wire each one's toggle to keep that set current.
@@ -9779,7 +9795,7 @@ $('seeall').addEventListener('click', async (e) => {
9779
9795
  if (link) link.disabled = true;
9780
9796
  if (v) v.textContent = 'Starting it…';
9781
9797
  let r = null, body = {};
9782
- try { r = await fetch(`/api/tasks/${it.run.taskId}/preview`, { method: 'POST' }); body = await r.json().catch(() => ({})); }
9798
+ try { r = await fetch(withKey(`/api/tasks/${it.run.taskId}/preview`), { method: 'POST' }); body = await r.json().catch(() => ({})); }
9783
9799
  catch (e) { body = { error: String(e.message ?? e) }; }
9784
9800
  if (link) link.disabled = false;
9785
9801
  const noteText = it.run.note || it.run.cmd || 'Starts when you press it.';
package/engine/lib.mjs CHANGED
@@ -1646,6 +1646,25 @@ export function detectRequestLanguage(text) {
1646
1646
  return /[぀-ヿ㐀-鿿]/.test(text || '') ? 'ja' : 'en';
1647
1647
  }
1648
1648
 
1649
+ export function testFailureReason({ text = '', count = 0, attempts = null } = {}) {
1650
+ const n = Number(count) || 0;
1651
+ const tries = attempts == null ? null : Number(attempts) || 0;
1652
+ if (detectRequestLanguage(text) === 'ja') {
1653
+ return tries == null ? `テスト失敗 ${n} 件` : `テスト失敗 ${n} 件(自動修正 ${tries} 回後も失敗)`;
1654
+ }
1655
+ const tests = `${n} test${n === 1 ? '' : 's'} failed`;
1656
+ return tries == null ? tests : `${tests} after ${tries} automatic fix attempt${tries === 1 ? '' : 's'}`;
1657
+ }
1658
+
1659
+ export function manualTestRetryPrompt({ text = '', count = 0, detail = '' } = {}) {
1660
+ const n = Number(count) || 0;
1661
+ const output = String(detail || '(no detailed output captured)').slice(0, 3200);
1662
+ if (detectRequestLanguage(text) === 'ja') {
1663
+ return `Retryが押されました。前回のテスト失敗 ${n} 件を確認し、元のタスクの作業環境で原因を修正してからテストを再実行してください。関係のないリファクタは行わないでください。\n\n前回のテスト出力:\n${output}`;
1664
+ }
1665
+ return `Retry was requested. Inspect the previous ${n} failing test${n === 1 ? '' : 's'}, fix the cause in the original task worktree, then rerun the tests. Do not make unrelated changes.\n\nPrevious test output:\n${output}`;
1666
+ }
1667
+
1649
1668
  // SLICE 2 intent routing (docs/HANDOFF-ONBOARDING-conversational.md): the single
1650
1669
  // composer is conversation-first — a chat/help/settings message ("what can you
1651
1670
  // do?", "reply in English", a greeting) must get a conversational reply, NOT a
@@ -2583,6 +2602,7 @@ export function workerPrompt(task, goal, lastFailure, agentName = 'claude-code')
2583
2602
  // (research, summaries, answers) simply skip this — the worker decides,
2584
2603
  // never us.
2585
2604
  `- 人が実際に動かして結果を見られる依頼なら、最後にプロジェクト直下へ ${RUN_DECL_FILE} を書く: {"cmd": "<起動コマンド>", "url": "<開くURL>", "note": "<何が見えるか1行>", "check": "<人に何を見て判断してほしいか1行>", "title": "<この作業のチケット名。対象が分かる程度に説明的で動作を含む短い名前。例「看板モードの追加」「ヘッダのズレの修正」。依頼文をそのまま写さない>"}。Manager はこれを使って人に「動かして見る」と「何を見ればいいか」を出す。動かして見せるものが無い依頼(調査・要約・質問への回答など)では書かなくてよい。`,
2605
+ `- 「ローカルで見れない」というフィードバックには、ターミナル操作や npx の再実行を案内して終わらないこと。${RUN_DECL_FILE} を実際に開ける内容へ更新し、可能なら自分でURLへ接続確認してから完了すること。`,
2586
2606
  // Links are rendered as links. The product cannot tell a real one from a
2587
2607
  // decorative one, so the only place this can be held is here.
2588
2608
  `- 自分が作っていない場所へのリンクを報告に書かない(存在しないURLや「詳しくはここ」のような飾りのリンクは、そのままリンクとして人に表示される)。`,
package/engine/server.mjs CHANGED
@@ -21,7 +21,7 @@ import { homedir } from 'node:os';
21
21
  import { fileURLToPath } from 'node:url';
22
22
  import { pathToFileURL } from 'node:url';
23
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
- 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';
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, testFailureReason, manualTestRetryPrompt, 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
27
  import { FALLBACK_CODEX_MODELS, loadCodexModels } from './codex-models.mjs';
@@ -2129,7 +2129,7 @@ async function startEphemeralApp(projectDir) {
2129
2129
  try {
2130
2130
  const key = readFileSync(join(home, 'secret.key'), 'utf8').trim();
2131
2131
  const r = await fetch(`http://localhost:${port}/api/state?key=${key}`);
2132
- if (r.ok) return { url: `http://localhost:${port}/?key=${key}`, kill: () => { try { child.kill(); } catch {} } };
2132
+ if (r.ok) return { url: `http://localhost:${port}/?key=${key}`, child, kill: () => { try { child.kill(); } catch {} } };
2133
2133
  } catch { /* not up yet */ }
2134
2134
  }
2135
2135
  try { child.kill(); } catch {}
@@ -2259,6 +2259,19 @@ async function startPreview(task) {
2259
2259
  live.expires = Date.now() + PREVIEW_IDLE_MS;
2260
2260
  return { url: live.url, reused: true };
2261
2261
  }
2262
+ // A Manager app cannot preview itself on its declared production port: that
2263
+ // port is already occupied, and its bare URL requires an access key. Boot the
2264
+ // edited checkout with isolated state and return its real key-bearing URL.
2265
+ let managerPackage = false;
2266
+ try { managerPackage = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')).name === '@galda/cli'; } catch {}
2267
+ if (managerPackage && existsSync(join(dir, 'engine', 'server.mjs')) && existsSync(join(dir, 'app', 'index.html'))) {
2268
+ const eph = await startEphemeralApp(dir);
2269
+ if (!eph) throw new Error('Could not start the local preview. Please try again.');
2270
+ const entry = { proc: eph.child, url: eph.url, expires: Date.now() + PREVIEW_IDLE_MS };
2271
+ previews.set(task.id, entry);
2272
+ eph.child.on('close', () => { if (previews.get(task.id) === entry) previews.delete(task.id); });
2273
+ return { url: eph.url, reused: false, started: true };
2274
+ }
2262
2275
  // Declared a URL but no command: the thing is already reachable (an app the
2263
2276
  // worker left running, a hosted page). Nothing to start.
2264
2277
  if (!cmd) return { url, reused: false, started: false };
@@ -3174,7 +3187,7 @@ async function verifyGate(goal, prProject, siblings, baseProject, activityTask =
3174
3187
  if (testsFailing) {
3175
3188
  const failure = recordGoalFailure(goal, {
3176
3189
  kind: 'test',
3177
- reason: `テスト失敗 ${blockingFailCount} 件`,
3190
+ reason: testFailureReason({ text: goal.text, count: blockingFailCount }),
3178
3191
  changedFiles,
3179
3192
  testResult: goal.testResult,
3180
3193
  });
@@ -3186,7 +3199,7 @@ async function verifyGate(goal, prProject, siblings, baseProject, activityTask =
3186
3199
  goal.autoRework = {
3187
3200
  ...(goal.autoRework ?? {}),
3188
3201
  testFailures: used + 1,
3189
- lastReason: `テスト失敗 ${blockingFailCount} 件`,
3202
+ lastReason: testFailureReason({ text: goal.text, count: blockingFailCount }),
3190
3203
  lastAt: new Date().toISOString(),
3191
3204
  };
3192
3205
  goal.blocked = null;
@@ -3232,7 +3245,7 @@ async function verifyGate(goal, prProject, siblings, baseProject, activityTask =
3232
3245
  }
3233
3246
  goal.blocked = {
3234
3247
  kind: gate.kind,
3235
- reason: testsFailing ? `テスト失敗 ${blockingFailCount} 件(自動修正 ${Number(goal.autoRework?.testFailures ?? 0)} 回後も失敗)`
3248
+ reason: testsFailing ? testFailureReason({ text: goal.text, count: blockingFailCount, attempts: goal.autoRework?.testFailures ?? 0 })
3236
3249
  : nothingVerifiable ? '検証条件つきのタスクなのに、ファイル変更も proof も無い(約束した検証が行われていない)'
3237
3250
  : '検証(proof)が全タスク分そろっていない(requireVerifyPass)',
3238
3251
  };
@@ -4525,8 +4538,8 @@ const server = createServer(async (req, res) => {
4525
4538
  return json(res, 200, goal);
4526
4539
  }
4527
4540
 
4528
- // Re-run the Manager-verified gate for a blocked goal (tests + proof), from
4529
- // Attention's 再検証 button. Passes review; still failing stays blocked.
4541
+ // Retry a test-blocked goal by sending the saved failure back to its worker.
4542
+ // Other blocked states still re-run the verification gate in place.
4530
4543
  const reverifyMatch = url.pathname.match(/^\/api\/goals\/(\d+)\/reverify$/);
4531
4544
  if (reverifyMatch && req.method === 'POST') {
4532
4545
  const goal = goals.find((g) => g.id === Number(reverifyMatch[1]));
@@ -4535,6 +4548,19 @@ const server = createServer(async (req, res) => {
4535
4548
  const project = projects.find((p) => p.id === goal.projectId);
4536
4549
  if (!project) return json(res, 404, { error: 'project not found' });
4537
4550
  const siblings = tasks.filter((t) => t.goalId === goal.id);
4551
+ if (goal.status === 'blocked' && goal.blocked?.kind === 'test') {
4552
+ const count = goal.testResult?.newFailures?.length ?? goal.testResult?.failed ?? 0;
4553
+ const prompt = manualTestRetryPrompt({ text: goal.text, count, detail: goal.testResult?.detail });
4554
+ goal.status = 'running';
4555
+ goal.blocked = null;
4556
+ goal.startedAt = null;
4557
+ // A manual retry is a fresh user decision, independent of the automatic
4558
+ // two-attempt budget. Keep the selected agent/model and same conversation.
4559
+ goal.autoRework = { ...(goal.autoRework ?? {}), testFailures: 0, manualRetryAt: new Date().toISOString() };
4560
+ saveGoal(goal);
4561
+ const task = queueReplyTask(goal, prompt, { from: 'manager', via: 'retry' });
4562
+ return json(res, 200, { goal, task, repairing: true });
4563
+ }
4538
4564
  const wdir = (goalWorkDirs.get(goal.id) && existsSync(goalWorkDirs.get(goal.id))) ? goalWorkDirs.get(goal.id) : project.dir;
4539
4565
  // 再検証 is the explicit human "run the tests now" — force the gate on for this
4540
4566
  // one goal even when the default (automatic) gate is off.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@galda/cli",
3
- "version": "0.10.57",
3
+ "version": "0.10.60",
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": {