@galda/cli 0.10.58 → 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
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';
@@ -3187,7 +3187,7 @@ async function verifyGate(goal, prProject, siblings, baseProject, activityTask =
3187
3187
  if (testsFailing) {
3188
3188
  const failure = recordGoalFailure(goal, {
3189
3189
  kind: 'test',
3190
- reason: `テスト失敗 ${blockingFailCount} 件`,
3190
+ reason: testFailureReason({ text: goal.text, count: blockingFailCount }),
3191
3191
  changedFiles,
3192
3192
  testResult: goal.testResult,
3193
3193
  });
@@ -3199,7 +3199,7 @@ async function verifyGate(goal, prProject, siblings, baseProject, activityTask =
3199
3199
  goal.autoRework = {
3200
3200
  ...(goal.autoRework ?? {}),
3201
3201
  testFailures: used + 1,
3202
- lastReason: `テスト失敗 ${blockingFailCount} 件`,
3202
+ lastReason: testFailureReason({ text: goal.text, count: blockingFailCount }),
3203
3203
  lastAt: new Date().toISOString(),
3204
3204
  };
3205
3205
  goal.blocked = null;
@@ -3245,7 +3245,7 @@ async function verifyGate(goal, prProject, siblings, baseProject, activityTask =
3245
3245
  }
3246
3246
  goal.blocked = {
3247
3247
  kind: gate.kind,
3248
- reason: testsFailing ? `テスト失敗 ${blockingFailCount} 件(自動修正 ${Number(goal.autoRework?.testFailures ?? 0)} 回後も失敗)`
3248
+ reason: testsFailing ? testFailureReason({ text: goal.text, count: blockingFailCount, attempts: goal.autoRework?.testFailures ?? 0 })
3249
3249
  : nothingVerifiable ? '検証条件つきのタスクなのに、ファイル変更も proof も無い(約束した検証が行われていない)'
3250
3250
  : '検証(proof)が全タスク分そろっていない(requireVerifyPass)',
3251
3251
  };
@@ -4538,8 +4538,8 @@ const server = createServer(async (req, res) => {
4538
4538
  return json(res, 200, goal);
4539
4539
  }
4540
4540
 
4541
- // Re-run the Manager-verified gate for a blocked goal (tests + proof), from
4542
- // 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.
4543
4543
  const reverifyMatch = url.pathname.match(/^\/api\/goals\/(\d+)\/reverify$/);
4544
4544
  if (reverifyMatch && req.method === 'POST') {
4545
4545
  const goal = goals.find((g) => g.id === Number(reverifyMatch[1]));
@@ -4548,6 +4548,19 @@ const server = createServer(async (req, res) => {
4548
4548
  const project = projects.find((p) => p.id === goal.projectId);
4549
4549
  if (!project) return json(res, 404, { error: 'project not found' });
4550
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
+ }
4551
4564
  const wdir = (goalWorkDirs.get(goal.id) && existsSync(goalWorkDirs.get(goal.id))) ? goalWorkDirs.get(goal.id) : project.dir;
4552
4565
  // 再検証 is the explicit human "run the tests now" — force the gate on for this
4553
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.58",
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": {