@galda/cli 0.10.73 → 0.10.74

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
@@ -576,7 +576,8 @@
576
576
  .riskfiles{font-family:var(--mono);font-size:10px;color:var(--ink3)}
577
577
  /* task 161/162: Review-row quick actions — aligned to the right of the row,
578
578
  vertically centered; icon (no emoji) reply; subtle inline reply box. */
579
- .revquick{display:flex;gap:4px;flex:0 0 auto;align-items:center;align-self:center;margin-left:8px}
579
+ .revquick{display:flex;gap:2px;flex:0 0 auto;align-items:center;align-self:center;margin-left:8px;opacity:0;transition:opacity .15s}
580
+ .task:hover .revquick,.digestrow:hover .revquick,.revquick:focus-within{opacity:1}
580
581
  .qbtn{width:26px;height:26px;flex:0 0 26px;border:1px solid var(--hair2);border-radius:7px;background:transparent;cursor:pointer;font-size:12px;line-height:0;display:grid;place-items:center;color:var(--ink2);padding:0}
581
582
  .qbtn svg{width:14px;height:14px}
582
583
  .qbtn.ok{color:var(--green);border-color:rgba(var(--green-rgb),.4)}
@@ -584,6 +585,15 @@
584
585
  .qbtn.no{color:var(--danger);border-color:rgba(var(--danger-rgb),.4)}
585
586
  .qbtn.no:hover{background:rgba(var(--danger-rgb),.12)}
586
587
  .qbtn:hover{color:var(--ink)}
588
+ /* Review行だけ: flagshipの .act/.b パターン(枠なし・ホバーで出現・面を塗らずグリフ色のみ) */
589
+ .revquick .qbtn{border:0;border-radius:6px}
590
+ .revquick .qbtn.ok{color:var(--ink3);border-color:transparent;background:transparent}
591
+ .revquick .qbtn.ok:hover{color:var(--green);background:transparent}
592
+ .revquick .qbtn.no{color:var(--ink3);border-color:transparent;background:transparent}
593
+ .revquick .qbtn.no:hover{color:var(--danger);background:transparent}
594
+ .revquick .qbtn.reply{color:var(--ink3)}
595
+ .revquick .qbtn.reply:hover{color:var(--ink)}
596
+ .revquick .qbtn:active{transform:scale(.92)}
587
597
  .qreplyrow{display:flex;gap:6px;margin-top:8px;width:100%}
588
598
  .qreplyrow[hidden]{display:none}
589
599
  .qreplyrow input{flex:1;min-width:0;height:30px;border:1px solid var(--hair);border-radius:8px;background:transparent;color:var(--ink);padding:0 10px;font:inherit;font-size:12px}
@@ -3418,6 +3428,8 @@ const FINISHED = (s) => ['done', 'partial', 'failed', 'interrupted'].includes(s)
3418
3428
  const ICONS = {
3419
3429
  pencil: '<svg viewBox="0 0 24 24"><path d="M4 20l1-4L16.5 4.5a2.1 2.1 0 013 3L8 19l-4 1z"/></svg>',
3420
3430
  reply: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 17l-5-5 5-5"/><path d="M4 12h11a4 4 0 0 1 4 4v3"/></svg>',
3431
+ check: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.5l4.2 4.2L19 7"/></svg>',
3432
+ close: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 6l12 12M18 6L6 18"/></svg>',
3421
3433
  up: '<svg viewBox="0 0 24 24"><path d="M12 19V5M5 12l7-7 7 7"/></svg>',
3422
3434
  down: '<svg viewBox="0 0 24 24"><path d="M12 5v14M5 12l7 7 7-7"/></svg>',
3423
3435
  trash: '<svg viewBox="0 0 24 24"><path d="M4 7h16M9 7V5a1 1 0 011-1h4a1 1 0 011 1v2M6.5 7l1 13h9l1-13M10 11v6M14 11v6"/></svg>',
@@ -4097,8 +4109,8 @@ function reviewDigestHtml(rows){
4097
4109
  <div class="digestmeta">${testBadge}${prLink}</div>
4098
4110
  </div>
4099
4111
  <div class="revquick">
4100
- <button type="button" class="qbtn ok" data-qapprove="${ids}" title="Approve">✓</button>
4101
- <button type="button" class="qbtn no" data-qdismiss="${ids}" title="Send back">✕</button>
4112
+ <button type="button" class="qbtn ok" data-qapprove="${ids}" title="Approve">${ICONS.check}</button>
4113
+ <button type="button" class="qbtn no" data-qdismiss="${ids}" title="Send back">${ICONS.close}</button>
4102
4114
  </div>
4103
4115
  </div>`;
4104
4116
  };
@@ -4686,8 +4698,8 @@ function renderTasks(){
4686
4698
  <div class="cond">${g.pr ? `<a class="prbig" href="${esc(g.pr)}" target="_blank">PR #${esc((g.pr.match(/pull\/(\d+)/) ?? [,''])[1])} ↗</a>` : ''}</div>
4687
4699
  <div class="qreplyrow" data-qreplyrow="${g.id}"${state.qreplyOpen.has(g.id) ? '' : ' hidden'}><input type="text" data-qreplyinput="${g.id}" placeholder="直したい点を書いて送信(Todoに戻して再作業)"><button type="button" class="qreplysend" data-qreplysend="${g.id}" aria-label="Send">${ICONS.up}</button></div></div>
4688
4700
  <div class="revquick">
4689
- <button type="button" class="qbtn ok" data-qapprove="${g.id}" title="Approve">✓</button>
4690
- <button type="button" class="qbtn no" data-qdismiss="${g.id}" title="Send back">✕</button>
4701
+ <button type="button" class="qbtn ok" data-qapprove="${g.id}" title="Approve">${ICONS.check}</button>
4702
+ <button type="button" class="qbtn no" data-qdismiss="${g.id}" title="Send back">${ICONS.close}</button>
4691
4703
  <button type="button" class="qbtn reply" data-qreply="${g.id}" title="Reply">${ICONS.reply}</button>
4692
4704
  </div></div>`;
4693
4705
  }).join('')
@@ -8358,9 +8370,12 @@ async function refresh(){
8358
8370
  // events — especially worker activity — don't saturate the main thread and drop
8359
8371
  // keystrokes while typing.
8360
8372
  let _renderT = null, _renderP = false;
8361
- function scheduleRender(){ if (_renderT) { _renderP = true; return; } render(); _renderT = setTimeout(() => { _renderT = null; if (_renderP) { _renderP = false; scheduleRender(); } }, 120); }
8373
+ // A thrown render() must not stall the throttle loop for good (live feed freeze,
8374
+ // e.g. on review reply) — catch it, log it, and keep scheduling so the next SSE
8375
+ // event still gets a paint attempt.
8376
+ function scheduleRender(){ if (_renderT) { _renderP = true; return; } try { render(); } catch (e) { console.error('[render]', e); } _renderT = setTimeout(() => { _renderT = null; if (_renderP) { _renderP = false; scheduleRender(); } }, 120); }
8362
8377
  let _actT = null, _actP = false;
8363
- function scheduleActRender(){ if (_actT) { _actP = true; return; } renderActPanel(); if (state.layout === 'flagship') renderFsFeed(); _actT = setTimeout(() => { _actT = null; if (_actP) { _actP = false; scheduleActRender(); } }, 200); }
8378
+ function scheduleActRender(){ if (_actT) { _actP = true; return; } try { renderActPanel(); if (state.layout === 'flagship') renderFsFeed(); } catch (e) { console.error('[actRender]', e); } _actT = setTimeout(() => { _actT = null; if (_actP) { _actP = false; scheduleActRender(); } }, 200); }
8364
8379
  // (connection refused = fatal, no auto-retry), so we own the reconnect loop.
8365
8380
  let esRetryMs = 1000;
8366
8381
  let sseUp = false; // live engine link — the honest signal behind the MCP tab's status dot (v46)
package/engine/lib.mjs CHANGED
@@ -1277,6 +1277,38 @@ export function taskStatusAfterVerify({ verdict, code, timedOut }) {
1277
1277
  return code === 0 ? 'done' : 'failed';
1278
1278
  }
1279
1279
 
1280
+ // task 450 (goal #783, task 448): a worker can exit 0 with changedFiles: []
1281
+ // and a result that reads as a thought cut off mid-sentence (e.g. "I'll wait
1282
+ // for this background full-suite run to complete rather than poll.") — no
1283
+ // work happened, but taskStatusAfterVerify (decided from `code` alone, before
1284
+ // changedFiles even exists) already called it 'done'. This is the same
1285
+ // principle the product already applies to verification (don't trust
1286
+ // self-report, judge by evidence) applied to task completion itself.
1287
+ // Deliberately narrow, per the same 0-file-legitimate-report carve-out as
1288
+ // isNothingVerifiable above: a real investigation/report task also has zero
1289
+ // changed files, but its result reads as a normal finished sentence, so only
1290
+ // text that also LOOKS cut off trips this.
1291
+ export function looksIncomplete(text) {
1292
+ const t = String(text ?? '').trim();
1293
+ if (!t) return true;
1294
+ if (t.length >= 200) return false;
1295
+ // A short blurb ending mid-word/mid-clause (no sentence-final punctuation
1296
+ // in any of ja/en) reads as truncated, not as a deliberate one-liner.
1297
+ if (!/[.!?。!?」))]$/.test(t)) return true;
1298
+ // goal-783/task-448's actual fragment ("I'll wait for this background
1299
+ // full-suite run to complete rather than poll.") IS a grammatically
1300
+ // complete sentence, so the check above misses it — but it's a one-shot
1301
+ // batch worker narrating a future intent ("I'll…"/"let me…"), never a
1302
+ // report of something actually done. That opener is itself the signal.
1303
+ return /^(i'll|i will|i'm going to|i am going to|let me)\b/i.test(t);
1304
+ }
1305
+
1306
+ export function isSuspiciouslyIncompleteDone({ status, changedFiles = [], result = '', hasVerdict = false } = {}) {
1307
+ if (status !== 'done' || hasVerdict) return false;
1308
+ if ((changedFiles ?? []).length > 0) return false;
1309
+ return looksIncomplete(result);
1310
+ }
1311
+
1280
1312
  // A task counts as complete for GOAL status when it cleanly finished
1281
1313
  // (done/skipped) OR it was force-stopped (interrupted) but its change is STRONGLY
1282
1314
  // verified — an independent passCondition verification PASSED (proof.verified &&
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, classifyWorkerFailure, isForcedStop, taskStatusAfterVerify, workerResultText, taskCountsAsComplete, resolveWantsPR, resolveGoalSource, buildGoalSummary, 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, classifyComposerIntentHeuristic, detectPauseIntent, buildIntentPrompt, parseIntentResponse, buildBoardSnapshot, validateBoardSnapshot, boardIsEmpty, decideBoardPull, resolveConnectedAgents, pickAvailableAgent, pickUtilityAgent, utilityModel, liveTakeoverDecision, shouldOpenBrowserOnBoot, goalTaskTitle, buildGoalTask, shouldUsePlannerForGoal, RUN_DECL_FILE, parseRunDeclaration, SHOT_DIR, collectShotNames, parseGoalAddress, REPLY_OUTCOMES, REPLY_INTENTS, buildReplyIntentPrompt, parseReplyIntent, buildGoalMessage, serializeGoalMessage, parseGoalMessages, computeInternalQualityMetrics, classifyInternalQualityTaskError, resolveWorkspaceMode, goalSessionFor, setGoalAgentSession, switchGoalAgentSession } from './lib.mjs';
24
+ import { parseStreamEvents, parseCodexEvents, buildCodexArgs, parsePlan, refinePlanTasks, canEditGoal, canDeleteGoal, shouldAskClarification, workerExitReason, classifyWorkerFailure, isForcedStop, taskStatusAfterVerify, workerResultText, taskCountsAsComplete, resolveWantsPR, resolveGoalSource, buildGoalSummary, 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, parseRunDeclaration, SHOT_DIR, collectShotNames, parseGoalAddress, REPLY_OUTCOMES, REPLY_INTENTS, buildReplyIntentPrompt, parseReplyIntent, buildGoalMessage, serializeGoalMessage, parseGoalMessages, computeInternalQualityMetrics, classifyInternalQualityTaskError, resolveWorkspaceMode, goalSessionFor, setGoalAgentSession, switchGoalAgentSession } from './lib.mjs';
25
25
  import { createSerialQueue } from './lib.mjs';
26
26
  import { isPrClosed } from './lib.mjs';
27
27
  import { collectProjectRules } from './lib.mjs';
@@ -2921,6 +2921,19 @@ async function runTask(task) {
2921
2921
  const after = await gitChanges(workDir);
2922
2922
  task.changedFiles = after.filter((l) => !before.includes(l)).map((l) => l.slice(3));
2923
2923
  sendProcessAct(task, `Detected ${task.changedFiles.length} changed file${task.changedFiles.length === 1 ? '' : 's'} from this task.`);
2924
+ // task 450: taskStatusAfterVerify decided 'done' from `code` alone, before
2925
+ // changedFiles existed to check against. Don't trust a clean exit code by
2926
+ // itself — a worker that changed nothing and left a cut-off-looking result
2927
+ // (goal #783 task 448: "I'll wait for this background full-suite run to
2928
+ // complete rather than poll.") did not actually finish. Downgrade to
2929
+ // 'interrupted' (retryable, surfaces in Attention) instead of silently
2930
+ // shipping it as done. Never touches a real 0-file report/investigation
2931
+ // task, whose result reads as a normal finished sentence.
2932
+ if (isSuspiciouslyIncompleteDone({ status: task.status, changedFiles: task.changedFiles, result: task.result, hasVerdict: Boolean(verdict) })) {
2933
+ task.status = 'interrupted';
2934
+ task.result = `[未完了の疑い: 変更ファイル0件かつ応答が途中で切れています] ${task.result}`.slice(0, RESULT_MAX);
2935
+ sendProcessAct(task, 'Result looked incomplete (0 changed files, cut-off text) — marked interrupted instead of done for human review.');
2936
+ }
2924
2937
  task.finishedAt = new Date().toISOString();
2925
2938
  task.resultAttemptId = task.attemptId;
2926
2939
  saveTask(task);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@galda/cli",
3
- "version": "0.10.73",
3
+ "version": "0.10.74",
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": {