@galda/cli 0.10.75 → 0.10.76

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
@@ -3354,12 +3354,14 @@ const withKey = (u) => {
3354
3354
  };
3355
3355
 
3356
3356
  // Theme names v3 (§6.5): English registered names only. Migrate persisted values
3357
- // (old internal codenames → v3 names; dark→midnight; archived glass→gradient default).
3357
+ // (old internal codenames → v3 names; dark→midnight; archived glass→midnight default).
3358
3358
  const THEMES = ['studio', 'gradient', 'banff', 'midnight', 'cinema'];
3359
3359
  const THEME_MIGRATE = { w4: 'studio', arc: 'gradient', nature: 'banff', glow: 'midnight', comp: 'cinema', dark: 'midnight', glass: 'gradient' };
3360
3360
  const savedTheme = (() => {
3361
3361
  const s = localStorage.getItem('theme');
3362
- const v = THEME_MIGRATE[s] ?? (THEMES.includes(s) ? s : 'gradient');
3362
+ // New installations start in Midnight. Existing valid values are preserved;
3363
+ // only a missing/invalid persisted value takes this default.
3364
+ const v = THEME_MIGRATE[s] ?? (THEMES.includes(s) ? s : 'midnight');
3363
3365
  if (v !== s) localStorage.setItem('theme', v);
3364
3366
  return v;
3365
3367
  })();
@@ -8702,6 +8704,11 @@ async function postOutboxItem(item){
8702
8704
  render();
8703
8705
  return; // finally still runs posting.delete(); skip land()/upsert()
8704
8706
  }
8707
+ // This optimistic turn was only for the short interval before the server
8708
+ // classified the input. A real goal is represented by the To Do lane and
8709
+ // its live Implementing log, never as a second message below the composer.
8710
+ const turns = state.chatTurns[item.projectId];
8711
+ if (turns) state.chatTurns[item.projectId] = turns.filter((turn) => turn.oid !== item.oid);
8705
8712
  goal.imageUrls = item.imageUrls;
8706
8713
  outbox.land(item.oid, goal.id);
8707
8714
  upsert(state.goals, goal);
@@ -8875,8 +8882,9 @@ async function send(){
8875
8882
  const fullText = batt.length
8876
8883
  ? `${text}${text ? '\n\n' : ''}添付画像(workerはこのパスをReadで確認できます): ${batt.map((a) => a.path).join(', ')}`
8877
8884
  : text;
8878
- // same housekeeping as a normal send: the words appear in the feed at once.
8879
- (state.chatTurns[state.active] ||= []).push({ role: 'you', text, ts: Date.now() });
8885
+ // A bound send belongs to this goal's persisted thread. Do not also add it
8886
+ // to the center conversation feed: that made a task reply appear below the
8887
+ // composer as an unrelated "You" turn.
8880
8888
  state.attach = [];
8881
8889
  state.msg = { mode: null, model: null, effort: null, skill: null, later: false, review: false };
8882
8890
  renderMsgHint();
@@ -8925,9 +8933,10 @@ async function send(){
8925
8933
  status: 'sending', ts: Date.now(),
8926
8934
  };
8927
8935
  outbox.add(item); // saved locally BEFORE any network call
8928
- // Pattern A (H23): the user's own turn enters the center feed immediately, so
8929
- // the composer reads like a conversation; if it's chat, a reply turn follows.
8930
- (state.chatTurns[state.active] ||= []).push({ role: 'you', text, ts: Date.now() });
8936
+ // The server decides whether this is a conversation or a real task. Show an
8937
+ // optimistic conversation turn while that decision is pending, then remove it
8938
+ // if the request becomes a task (tasks belong in To Do / Implementing only).
8939
+ (state.chatTurns[state.active] ||= []).push({ role: 'you', text, oid: item.oid, ts: Date.now() });
8931
8940
  state.attach = [];
8932
8941
  // mode/skill/later/review are per-message → clear after send (model stays sticky).
8933
8942
  state.msg = { mode: null, model: null, effort: null, skill: null, later: false, review: false };
package/engine/lib.mjs CHANGED
@@ -1306,6 +1306,10 @@ export function looksIncomplete(text) {
1306
1306
  export function isSuspiciouslyIncompleteDone({ status, changedFiles = [], result = '', hasVerdict = false } = {}) {
1307
1307
  if (status !== 'done' || hasVerdict) return false;
1308
1308
  if ((changedFiles ?? []).length > 0) return false;
1309
+ // A worker that only asks for permission to start a local server did not
1310
+ // produce a locally checkable result. Treating its clean exit as success
1311
+ // re-opened the old PR/review summary and hid this real blocker (#788).
1312
+ if (/\b(permission|approve running|approval required)\b|許可.*(必要|ください)|実行してよろしい/i.test(String(result))) return true;
1309
1313
  return looksIncomplete(result);
1310
1314
  }
1311
1315
 
@@ -2382,65 +2386,6 @@ export function dismissGoal({ goalStatus, outcome = 'continue' } = {}) {
2382
2386
  return { ok: true, outcome: 'continue', status: 'running', spawnsWorker: true };
2383
2387
  }
2384
2388
 
2385
- // 誰が決めるか=**AI本人** (docs/design/ONE-CONVERSATION.md §1.4)
2386
- //
2387
- // 返事が来た時、それが「このまま直して」なのか「考え直したい」なのか「別件」なのか
2388
- // 「ただ聞いただけ」なのかを、**我々の層で当てない**。キーワードでも類似度でもなく、
2389
- // 人の言葉を1回だけ AI に渡して、返ってきた答えのとおりに配線する。0.72 の自動合流を
2390
- // 消したのと同じ理由——この層が人の意図を推測し始めると、外れた時に黙って別のことを
2391
- // する(今日そこを1つずつ潰してきた)。
2392
- //
2393
- // 迷った時だけ人に聞く: AI が unsure と答えたら**ゴールに触らず**選択肢を返す。人は
2394
- // モードを普段選ばない。迷った時だけ選ぶ。
2395
- export const REPLY_INTENTS = [...REPLY_OUTCOMES, 'unsure'];
2396
- export function buildReplyIntentPrompt({ goalText, reply } = {}) {
2397
- return [
2398
- 'You route replies in Agent Manager: a person delegated a coding task, an AI did it, and it came back for review. The person has now replied.',
2399
- 'Decide what the reply ASKS FOR — exactly one of:',
2400
- '- "continue": fix or change the work that was just done (the usual case).',
2401
- '- "rescope": stop and rethink; the request itself should now say something different.',
2402
- '- "split": a separate piece of work that should not touch this one.',
2403
- '- "answer": a question about the work. Nothing should be built or changed — they want to know something.',
2404
- '- "unsure": you genuinely cannot tell. Prefer this over guessing: the person will be shown the choices.',
2405
- 'Judge the reply on its own words. Do not treat a question mark as proof of "answer", or an imperative as proof of "continue" — "why is this red? make it blue" asks for a change.',
2406
- 'Output ONLY one line of raw JSON: {"outcome":"continue|rescope|split|answer|unsure"}. No markdown, no code fence, no other text.',
2407
- '',
2408
- 'The request that was worked on:',
2409
- String(goalText ?? '').slice(0, 2000),
2410
- '',
2411
- 'Their reply:',
2412
- String(reply ?? '').slice(0, 4000),
2413
- ].join('\n');
2414
- }
2415
-
2416
- // Tolerant parser — never throws. It returns TWO things, and the difference
2417
- // between them is the whole point:
2418
- //
2419
- // read: true … the AI answered, and we understood it. 'unsure' here means the
2420
- // AI genuinely could not tell → ask the person (§1.4).
2421
- // read: false … we could not get an answer at all (no agent installed, offline,
2422
- // rate-limited, garbled output). The AI never said anything, so
2423
- // there is nobody to have been unsure.
2424
- //
2425
- // Treating those two the same is a real bug, found by an existing test on
2426
- // 2026-07-22: on a machine with no agent, a reply stopped doing ANYTHING — it put
2427
- // the four choices on screen and waited. But if the agent cannot be reached, no
2428
- // answer can be written either; the only useful thing a reply can still do is
2429
- // what it always did — go back as rework, and run when the agent returns.
2430
- // So the caller falls back to 'continue' when read is false. That is not a guess
2431
- // about what the person meant: it is the documented default this endpoint has
2432
- // always had when no outcome is given.
2433
- export function parseReplyIntent(raw) {
2434
- const unread = { outcome: 'unsure', read: false };
2435
- if (!raw || typeof raw !== 'string') return unread;
2436
- const m = raw.match(/\{[\s\S]*\}/);
2437
- if (!m) return unread;
2438
- let obj;
2439
- try { obj = JSON.parse(m[0]); } catch { return unread; }
2440
- if (!REPLY_INTENTS.includes(obj?.outcome)) return unread; // answered, but not in our words
2441
- return { outcome: obj.outcome, read: true };
2442
- }
2443
-
2444
2389
  // 宛先(docs/design/ONE-CONVERSATION.md §1.2)
2445
2390
  //
2446
2391
  // チャットで「#12 の件だけど」と書けたら、そのチケット宛て。**決定論で、我々の推測は
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, 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';
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, 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';
@@ -5214,24 +5214,6 @@ const server = createServer(async (req, res) => {
5214
5214
  return json(res, 200, payload);
5215
5215
  }
5216
5216
 
5217
- // 返事の行き先を AI 本人に決めてもらう (ONE-CONVERSATION.md §1.4)。
5218
- //
5219
- // 判定は1往復だけの軽い仕事なので、作業した本人ではなくユーティリティのモデルに
5220
- // 聞く(答えを書く answer 本体は本人に聞く——あちらは中身を知っている必要がある)。
5221
- // 呼べなかった時は 'unsure'。「たぶん continue」で走り出すより、人に1タップ選ばせる
5222
- // ほうが安い。
5223
- async function judgeReplyIntent(goal, reply) {
5224
- let parsed;
5225
- try {
5226
- const r = await runUtility({ prompt: buildReplyIntentPrompt({ goalText: goal.text, reply }), cwd: ROOT, tools: 'Read' });
5227
- parsed = parseReplyIntent(r.result);
5228
- } catch { parsed = { outcome: 'unsure', read: false }; }
5229
- // Could not ask at all → do what a reply has always done (continue). Blocking the
5230
- // reply on four choices would leave a machine with no reachable agent unable to
5231
- // send anything back, and an answer could not have been written there anyway.
5232
- return parsed.read ? parsed.outcome : 'continue';
5233
- }
5234
-
5235
5217
  // 「聞いてるだけ」への返事 (ONE-CONVERSATION.md §1.3 answer)。
5236
5218
  //
5237
5219
  // 答えるのは**作業した AI 本人**=その agent の session を resume する。状況一覧しか
@@ -5285,26 +5267,20 @@ async function answerGoalQuestion(goal, question) {
5285
5267
  let body = '';
5286
5268
  req.on('data', (d) => { body += d; });
5287
5269
  req.on('end', async () => {
5288
- let text = '', outcome = 'continue', auto = false, requested = {};
5270
+ let text = '', outcome = 'continue', requested = {};
5289
5271
  try {
5290
5272
  requested = JSON.parse(body || '{}');
5291
5273
  text = String(requested.text ?? '').trim();
5292
5274
  if (REPLY_OUTCOMES.includes(requested.outcome)) outcome = requested.outcome;
5293
- auto = requested.outcome === 'auto';
5294
5275
  } catch {}
5295
5276
  const worker = applyReplyWorker(goal, requested);
5296
5277
  if (!worker.ok) return json(res, 409, { error: worker.error });
5297
5278
 
5298
- // outcome:'auto' = 決めるのは AI 本人 (§1.4)。人の言葉を1回だけ渡して、返って
5299
- // きた行き先のとおりに配線する。我々はキーワードも類似度も見ない。
5300
- // 迷ったと言われたら**ゴールに触らずに**選択肢を返す——ここで勝手に continue
5301
- // 倒すと、質問しただけの人のコードが動き出す(それが直したかった壊れ方)。
5302
- if (auto) {
5303
- if (!text) return json(res, 400, { error: 'text required' });
5304
- const judged = await judgeReplyIntent(goal, text);
5305
- if (judged === 'unsure') return json(res, 200, { goal, outcome: 'unsure', choices: REPLY_OUTCOMES });
5306
- outcome = judged;
5307
- }
5279
+ // A review reply is transport, not a classification request. Legacy clients
5280
+ // send outcome:'auto'; it deliberately means the deterministic default:
5281
+ // preserve the exact words and resume this goal's agent session. Specialized
5282
+ // outcomes work only when an explicit UI action sends one of REPLY_OUTCOMES.
5283
+ if (requested.outcome === 'auto' && !text) return json(res, 400, { error: 'text required' });
5308
5284
  // 返事の行き先4種 (ONE-CONVERSATION.md §1.3). 既定は今までどおり continue なので、
5309
5285
  // outcome を送ってこない今の UI の挙動は1ミリも変わらない。
5310
5286
  const result = dismissGoal({ goalStatus: goal.status, outcome });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@galda/cli",
3
- "version": "0.10.75",
3
+ "version": "0.10.76",
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": {