@galda/cli 0.10.97 → 0.10.99

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
@@ -1389,6 +1389,9 @@
1389
1389
  Was a pulsing dot (fsrundot). Centre WITHOUT transform (spin's rotate would override a centring
1390
1390
  translate → drift bottom-right) via top/left:50% + negative-half margin; concentric with the 4px dot. */
1391
1391
  #fsRoot .fsproj.run i{background:none;box-shadow:none;position:relative}
1392
+ /* .fsproj.on i and .fsproj.run i have equal specificity. Keep the running
1393
+ ring visible when the currently selected project is also running. */
1394
+ #fsRoot .fsproj.on.run i{background:none;box-shadow:none}
1392
1395
  /* Review-blue comparison baseline: the Doing cue is a 7px / 1.3px broken
1393
1396
  ring, while idle and Review remain 4px dots. */
1394
1397
  #fsRoot .fsproj.run i::after{content:"";position:absolute;top:50%;left:50%;width:7px;height:7px;margin:-3.5px 0 0 -3.5px;box-sizing:border-box;border-radius:50%;border:1.3px solid var(--green);border-top-color:transparent;animation:spin 1.8s linear infinite}
@@ -1920,7 +1923,7 @@
1920
1923
  box-shadow:inset 0 0 0 1px rgba(var(--green-rgb),.30);font:600 12px/1 var(--ui);cursor:pointer;display:inline-flex;align-items:center;gap:7px}
1921
1924
  .sa-app:hover{box-shadow:inset 0 0 0 1px var(--green);background:rgba(var(--green-rgb),.12)}
1922
1925
  .sa-app svg{width:12px;height:12px;fill:none;stroke:currentColor;stroke-width:2.4;stroke-linecap:round;stroke-linejoin:round}
1923
- .sa-approve-wrap{display:flex;flex-direction:column;align-items:flex-start;gap:5px;flex:0 0 auto}
1926
+ .sa-approve-wrap{display:flex;flex-direction:column;align-items:center;gap:5px;flex:0 0 auto}
1924
1927
  .sa-approve-wrap + .sa-dis{align-self:flex-start}
1925
1928
  .sa-delivery{font:500 9.5px/1.15 var(--mono);color:var(--ink3);text-decoration:none;white-space:nowrap}
1926
1929
  a.sa-delivery{color:var(--green)}
@@ -6532,8 +6535,11 @@ function renderFsRail(){
6532
6535
  // project the To Do/Kanban view showed as Doing kept a static dot in the rail
6533
6536
  // (Masa report 2026-07-27: "全部doingなのに緑のアイコンが回ってない"). Match the
6534
6537
  // same bucket the board/list already use, minus Paused (intentionally static).
6535
- const running = pgoals.some((g) => fsBoardBucket(g, ptasks) === 'doing'
6536
- && !(g.status === 'blocked' && g.blocked?.kind === 'paused'));
6538
+ // A live task is the strongest running signal. Keep the goal-bucket fallback
6539
+ // for Doing goals that have not created their first task yet.
6540
+ const running = ptasks.some((t) => t.status === 'running')
6541
+ || pgoals.some((g) => fsBoardBucket(g, ptasks) === 'doing'
6542
+ && !(g.status === 'blocked' && g.blocked?.kind === 'paused'));
6537
6543
  const td = ptasks.filter((t) => ['running', 'queued'].includes(t.status)).length;
6538
6544
  // The rail's eye icon is the review/attention count. Needs-you goals
6539
6545
  // (needsInput/needsApproval/blocked, minus Paused) block on the person the
@@ -9985,8 +9991,23 @@ function saItemHtml(it, i){
9985
9991
  const runDeclErrorBody = (!((it.run?.cmd || it.run?.url)) && it.runDeclError)
9986
9992
  ? `<div class="ropen rdeclerr"><p class="rbody v">${failureLang() === 'ja' ? 'AIが開けるものを用意しようとしましたが、記録が読み取れませんでした。返信して直させてください。' : 'The AI tried to declare something to open here, but the record was unreadable. Reply to have it rewritten.'}</p></div>`
9987
9993
  : '';
9994
+ // The worker declared a static file AND changed something else (engine sets
9995
+ // run.alsoChanged; goal #871). Both arrive as a /upload link, so the address
9996
+ // above cannot tell you whether you are about to open the product or a page
9997
+ // the agent wrote about the product — #871 was the second, and the request
9998
+ // ("something I can check locally") had to be made again.
9999
+ //
10000
+ // One sentence of fact in the same voice as the note beside it: no badge, no
10001
+ // colour, no warning tone. The card is not saying the worker did the wrong
10002
+ // thing — a write-up beside a code change is often exactly right. It is
10003
+ // saying which of the two this link is. Absent → nothing renders (§1.5-3).
10004
+ const alsoChanged = it.run?.alsoChanged?.length
10005
+ ? `<p class="rbody v">${failureLang() === 'ja'
10006
+ ? `AIが書いたファイルを開きます。このタスクは ${esc(it.run.alsoChanged.join(' · '))} も変更しています。`
10007
+ : `Opens a file the agent wrote. This task also changed ${esc(it.run.alsoChanged.join(' · '))}.`}</p>`
10008
+ : '';
9988
10009
  const openBody = (it.run?.cmd || it.run?.url)
9989
- ? `<div class="ropen" id="runlocal${i}"><button type="button" class="olink" data-saact="preview:${i}" id="prevlink${i}">${esc(it.run.url || 'Start it')}<span class="ar">↗</span></button><p class="rbody v">${esc(it.run.note || it.run.cmd || 'Starts when you press it.')}${it.interruptedTask ? ' Available now; it does not wait for this run to finish.' : ''}</p></div>`
10010
+ ? `<div class="ropen" id="runlocal${i}"><button type="button" class="olink" data-saact="preview:${i}" id="prevlink${i}">${esc(it.run.url || 'Start it')}<span class="ar">↗</span></button><p class="rbody v">${esc(it.run.note || it.run.cmd || 'Starts when you press it.')}${it.interruptedTask ? ' Available now; it does not wait for this run to finish.' : ''}</p>${alsoChanged}</div>`
9990
10011
  : it.pr
9991
10012
  ? `<div class="ropen"><a class="olink" href="${esc(it.pr)}" target="_blank" rel="noopener">${esc(it.pr)}<span class="ar">↗</span></a><p class="rbody v">Review and merge this pull request to finish the item.</p></div>`
9992
10013
  : runDeclErrorBody;
@@ -10316,6 +10337,24 @@ function saRender(){
10316
10337
  const previousBody = S.querySelector('.sa-body');
10317
10338
  const previousScrollTop = previousBody?.scrollTop ?? 0;
10318
10339
  const restoreScroll = Boolean(previousBody) && SA.focusGid == null;
10340
+ // A reply changes the height of the card being replied to (new thread line, status
10341
+ // change) — restoring a raw scrollTop pixel then leaves the viewport at the same
10342
+ // number but showing DIFFERENT content, a visible jolt on whatever card the person
10343
+ // was actually reading (2026-07-29). Anchor on that card's own goalId instead: find
10344
+ // the topmost visible card before the rebuild, and after the rebuild scroll so that
10345
+ // SAME card sits at the SAME on-screen position, regardless of how much other cards
10346
+ // above it grew or shrank.
10347
+ let anchorGid = null, anchorOffset = 0;
10348
+ if (restoreScroll) {
10349
+ const containerTop = previousBody.getBoundingClientRect().top;
10350
+ // The card actually being read sits near the middle of the viewport, not whatever
10351
+ // sliver of a previous card's bottom edge still peeks in at the very top.
10352
+ const midY = containerTop + previousBody.clientHeight / 2;
10353
+ for (const el of previousBody.querySelectorAll('.sa-item')) {
10354
+ const r = el.getBoundingClientRect();
10355
+ if (r.top <= midY && r.bottom >= midY) { anchorGid = el.dataset.gid; anchorOffset = r.top - containerTop; break; }
10356
+ }
10357
+ }
10319
10358
  const focusedReplyGoal = document.activeElement?.matches?.('#seeall .sa-chat')
10320
10359
  ? document.activeElement.dataset.goal
10321
10360
  : null;
@@ -10340,7 +10379,6 @@ function saRender(){
10340
10379
  SA.lastMarkup = markup;
10341
10380
  S.innerHTML = markup;
10342
10381
  const body = S.querySelector('.sa-body');
10343
- if (body && restoreScroll) body.scrollTop = previousScrollTop;
10344
10382
  // Once the person scrolls, their position wins over the short opening-focus
10345
10383
  // animation. Programmatic opening scroll fires this too, which is fine: the
10346
10384
  // first placement happened and later SSE ticks should preserve it.
@@ -10352,7 +10390,18 @@ function saRender(){
10352
10390
  // Native <details> would collapse on every rebuild (SSE tick, approve, etc.) —
10353
10391
  // track what's open per goal+section in SA.detOpen (persists across re-renders)
10354
10392
  // and wire each one's toggle to keep that set current.
10355
- paintReplyNote(); // the answer/question under a card's reply box survives its rebuild
10393
+ paintReplyNote(); // the answer/question under a card's reply box survives its rebuild — this is
10394
+ // also what actually grows a replied-to card's height, so the scroll anchor
10395
+ // below must run AFTER it, not before (2026-07-29 jolt fix).
10396
+ if (body && restoreScroll) {
10397
+ const anchorEl = anchorGid != null ? body.querySelector(`.sa-item[data-gid="${anchorGid}"]`) : null;
10398
+ if (anchorEl) {
10399
+ const newOffset = anchorEl.getBoundingClientRect().top - body.getBoundingClientRect().top;
10400
+ body.scrollTop += (newOffset - anchorOffset);
10401
+ } else {
10402
+ body.scrollTop = previousScrollTop;
10403
+ }
10404
+ }
10356
10405
  for (const det of S.querySelectorAll('[data-detkey]')) {
10357
10406
  det.addEventListener('toggle', () => {
10358
10407
  const k = det.dataset.detkey;
@@ -63,16 +63,26 @@ function normalizeEvidence(value) {
63
63
  const requirementId = cleanText(value?.requirementId);
64
64
  const attemptId = cleanText(value?.attemptId);
65
65
  if (!requirementId || !attemptId) return null;
66
- const claim = CLAIM_STATUSES.has(value.claim) ? value.claim : 'not-addressed';
66
+ const claim = CLAIM_STATUSES.has(value.claim) ? value.claim : null;
67
67
  const coverage = COVERAGE_STATUSES.has(value.coverage) ? value.coverage : 'unverified';
68
+ const workerEvidence = cleanText(value.workerEvidence ?? value.evidence);
69
+ const workerFiles = [...new Set([
70
+ ...(Array.isArray(value.workerFiles) ? value.workerFiles : []),
71
+ ...(Array.isArray(value.files) ? value.files : []),
72
+ ].filter((item) => typeof item === 'string' && item))];
73
+ const observedFiles = [...new Set((Array.isArray(value.observedFiles) ? value.observedFiles : []).filter((item) => typeof item === 'string' && item))];
68
74
  return {
69
75
  requirementId,
70
76
  attemptId,
71
77
  taskId: value.taskId ?? null,
72
78
  claim,
73
79
  coverage,
74
- evidence: cleanText(value.evidence),
75
- files: [...new Set(Array.isArray(value.files) ? value.files.filter((item) => typeof item === 'string' && item) : [])],
80
+ evidence: workerEvidence,
81
+ files: [...new Set([...workerFiles, ...observedFiles])],
82
+ workerEvidence,
83
+ workerFiles,
84
+ observedFiles,
85
+ checks: Array.isArray(value.checks) ? value.checks : [],
76
86
  };
77
87
  }
78
88
 
@@ -66,3 +66,56 @@ export function mergeRequirementVerificationEvidence(existing = [], incoming = [
66
66
  }
67
67
  return [...rows.values()];
68
68
  }
69
+
70
+ /**
71
+ * Translate Manager-owned observations into explicitly requirement-scoped
72
+ * checks. Merely seeing files or successfully starting a preview is useful
73
+ * evidence, but it is not proof that the requested behavior is correct.
74
+ */
75
+ export function buildManagerVerificationChecks({
76
+ requirementIds = [], testResult = null, testsFailing = false, proof = null,
77
+ run = null, previewVerification = null, changedFiles = [],
78
+ } = {}) {
79
+ const ids = cleanList(requirementIds);
80
+ const files = cleanList(changedFiles);
81
+ const checks = [];
82
+
83
+ if (files.length) {
84
+ checks.push({
85
+ kind: 'changed-files', status: 'observed', requirementIds: ids, files,
86
+ detail: `${files.length} changed file${files.length === 1 ? '' : 's'} observed`,
87
+ });
88
+ }
89
+
90
+ if (testResult) {
91
+ let status = 'observed';
92
+ if (!testResult.ran || testResult.skipped) status = 'not-applicable';
93
+ else if (testsFailing) status = 'failed';
94
+ else if (testResult.ok === true && !testResult.inconclusive && !testResult.infraOnly && !testResult.preExistingOnly) status = 'verified';
95
+ checks.push({
96
+ kind: 'test', status, requirementIds: ids,
97
+ detail: testResult.detail || testResult.skippedReason || `${testResult.passed ?? 0} passed, ${testResult.failed ?? 0} failed`,
98
+ });
99
+ }
100
+
101
+ if (proof) {
102
+ checks.push({
103
+ kind: 'proof',
104
+ status: proof.verified === true ? (proof.pass === true ? 'verified' : 'failed') : 'observed',
105
+ requirementIds: ids,
106
+ detail: proof.detail || 'Proof artifact observed',
107
+ });
108
+ }
109
+
110
+ if (run || previewVerification) {
111
+ checks.push({
112
+ kind: 'open-it', status: 'observed', requirementIds: ids,
113
+ detail: previewVerification?.detail || (previewVerification?.status === 'started'
114
+ ? 'Local preview started; visual correctness still requires review'
115
+ : 'Worker declared a local preview'),
116
+ ...(previewVerification?.url || run?.url ? { url: previewVerification?.url || run.url } : {}),
117
+ });
118
+ }
119
+
120
+ return checks;
121
+ }
package/engine/server.mjs CHANGED
@@ -23,6 +23,7 @@ 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, 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, 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
+ import { buildManagerVerificationChecks, buildRequirementVerificationEvidence, mergeRequirementVerificationEvidence } from './requirement-verification.mjs';
26
27
  import { createSerialQueue } from './lib.mjs';
27
28
  import { parseRequirementEvidence, unmappedChangedFiles, previewOpensSomethingElse } from './lib.mjs';
28
29
  import { isPrClosed } from './lib.mjs';
@@ -3854,6 +3855,26 @@ function prSafetyIssue(goal, project, files) {
3854
3855
  return classifyPrSafety({ baseline, files, currentFiles: gitStatusPaths(lines), currentBranch });
3855
3856
  }
3856
3857
 
3858
+ function syncGoalRequirementVerification(goal, siblings, { testsFailing = false } = {}) {
3859
+ const incoming = siblings.flatMap((task) => buildRequirementVerificationEvidence({
3860
+ requirementIds: task.activeRequirementIds ?? [],
3861
+ attemptId: task.attemptId || `task-${task.id}`,
3862
+ taskId: task.id,
3863
+ workerEvidence: task.requirementEvidence,
3864
+ changedFiles: task.changedFiles ?? [],
3865
+ checks: buildManagerVerificationChecks({
3866
+ requirementIds: task.activeRequirementIds ?? [],
3867
+ testResult: goal.testResult,
3868
+ testsFailing,
3869
+ proof: task.proof,
3870
+ run: task.run,
3871
+ previewVerification: task.previewVerification,
3872
+ changedFiles: task.changedFiles ?? [],
3873
+ }),
3874
+ }));
3875
+ goal.requirementEvidence = mergeRequirementVerificationEvidence(goal.requirementEvidence ?? [], incoming);
3876
+ }
3877
+
3857
3878
  // The Manager-verified review gate (shared by goal completion and /reverify):
3858
3879
  // run OUR OWN tests, generate a plain-language headline, and require proof for a
3859
3880
  // UI change — capturing it (3 tries) if missing. Any failure → 'blocked' (shown
@@ -3975,6 +3996,10 @@ async function verifyGate(goal, prProject, siblings, baseProject, activityTask =
3975
3996
  // Count that drives the block message: the NEW failures when we could diff
3976
3997
  // against the base, else the raw total (fail-closed when the base is unknown).
3977
3998
  const blockingFailCount = goal.testResult.newFailures?.length ?? goal.testResult.failed;
3999
+ // Persist the Manager's own requirement-scoped observations before any
4000
+ // automatic rework return. A worker claim alone never becomes verified.
4001
+ syncGoalRequirementVerification(goal, siblings, { testsFailing });
4002
+ saveGoal(goal);
3978
4003
  // §4補足: requireVerifyPass (opt-in, off by default) makes the gate strict —
3979
4004
  // EVERY task must carry a passing proof, not just UI tasks. Only ever tightens;
3980
4005
  // never conflicts with §6 (still can't reach Done without a human).
@@ -5036,14 +5061,27 @@ const server = createServer(async (req, res) => {
5036
5061
  const goal = goals.find((g) => g.id === task.goalId);
5037
5062
  startPreview(task)
5038
5063
  .then((p) => {
5064
+ task.previewVerification = {
5065
+ status: 'started', url: p.url, at: new Date().toISOString(),
5066
+ detail: 'Local preview started; visual correctness still requires review',
5067
+ };
5068
+ saveTask(task);
5039
5069
  if (goal?.verificationInfraError?.source === 'preview') {
5040
5070
  goal.verificationInfraError = null;
5071
+ }
5072
+ if (goal) {
5073
+ syncGoalRequirementVerification(goal, tasks.filter((item) => item.goalId === goal.id), {
5074
+ testsFailing: Boolean(goal.testResult?.ran && goal.testResult.failed > 0
5075
+ && !goal.testResult.inconclusive && !goal.testResult.infraOnly && !goal.testResult.preExistingOnly),
5076
+ });
5041
5077
  saveGoal(goal);
5042
5078
  }
5043
5079
  json(res, 200, p);
5044
5080
  })
5045
5081
  .catch((e) => {
5046
5082
  const detail = String(e.message ?? e).slice(0, 200);
5083
+ task.previewVerification = { status: 'infra-error', detail, at: new Date().toISOString() };
5084
+ saveTask(task);
5047
5085
  if (goal && goal.status === 'review') {
5048
5086
  goal.verificationInfraError = buildVerificationInfraError({
5049
5087
  source: 'preview',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@galda/cli",
3
- "version": "0.10.97",
3
+ "version": "0.10.99",
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": {