@galda/cli 0.10.74 → 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
@@ -2232,6 +2232,12 @@
2232
2232
  .redesign .ghmore .chev path{stroke:currentColor;stroke-width:1.8;fill:none;stroke-linecap:round;stroke-linejoin:round}
2233
2233
  .redesign .ghmore.open .chev{transform:rotate(180deg)}
2234
2234
  .redesign .ghmore .n{color:var(--ink2);font-variant-numeric:tabular-nums}
2235
+ /* Long-Request fold (Masa 2026-07-27, overrides spec §4 "no accordion" — that rule
2236
+ assumed Request is always the user's short ask; a mispaste of another card's whole
2237
+ content proved that assumption false). No bespoke widget: it reuses the SAME `.lacc`
2238
+ pulldown as Activity/Conversation below (§3 — chevron on the first line, no left
2239
+ rule, offset-indented body when open), rather than the diff-fold's fade+button shape
2240
+ ("他の›と同じルールじゃなくない?" — Masa 2026-07-27). No extra CSS needed here. */
2235
2241
  /* meta links carry NO underline — PR is a real <a href> (spec §5, Masa 2026-07-20) */
2236
2242
  .redesign .rmeta a{text-decoration:none}
2237
2243
  /* Request / What-to-check = full contrast, same ink as the title (spec §2, no fade) */
@@ -3348,12 +3354,14 @@ const withKey = (u) => {
3348
3354
  };
3349
3355
 
3350
3356
  // Theme names v3 (§6.5): English registered names only. Migrate persisted values
3351
- // (old internal codenames → v3 names; dark→midnight; archived glass→gradient default).
3357
+ // (old internal codenames → v3 names; dark→midnight; archived glass→midnight default).
3352
3358
  const THEMES = ['studio', 'gradient', 'banff', 'midnight', 'cinema'];
3353
3359
  const THEME_MIGRATE = { w4: 'studio', arc: 'gradient', nature: 'banff', glow: 'midnight', comp: 'cinema', dark: 'midnight', glass: 'gradient' };
3354
3360
  const savedTheme = (() => {
3355
3361
  const s = localStorage.getItem('theme');
3356
- 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');
3357
3365
  if (v !== s) localStorage.setItem('theme', v);
3358
3366
  return v;
3359
3367
  })();
@@ -8696,6 +8704,11 @@ async function postOutboxItem(item){
8696
8704
  render();
8697
8705
  return; // finally still runs posting.delete(); skip land()/upsert()
8698
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);
8699
8712
  goal.imageUrls = item.imageUrls;
8700
8713
  outbox.land(item.oid, goal.id);
8701
8714
  upsert(state.goals, goal);
@@ -8869,8 +8882,9 @@ async function send(){
8869
8882
  const fullText = batt.length
8870
8883
  ? `${text}${text ? '\n\n' : ''}添付画像(workerはこのパスをReadで確認できます): ${batt.map((a) => a.path).join(', ')}`
8871
8884
  : text;
8872
- // same housekeeping as a normal send: the words appear in the feed at once.
8873
- (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.
8874
8888
  state.attach = [];
8875
8889
  state.msg = { mode: null, model: null, effort: null, skill: null, later: false, review: false };
8876
8890
  renderMsgHint();
@@ -8919,9 +8933,10 @@ async function send(){
8919
8933
  status: 'sending', ts: Date.now(),
8920
8934
  };
8921
8935
  outbox.add(item); // saved locally BEFORE any network call
8922
- // Pattern A (H23): the user's own turn enters the center feed immediately, so
8923
- // the composer reads like a conversation; if it's chat, a reply turn follows.
8924
- (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() });
8925
8940
  state.attach = [];
8926
8941
  // mode/skill/later/review are per-message → clear after send (model stays sticky).
8927
8942
  state.msg = { mode: null, model: null, effort: null, skill: null, later: false, review: false };
@@ -9499,6 +9514,24 @@ function saMsgRowsHtml(msgs, expanded){
9499
9514
  // word-level highlight (h) and, for a huge change, a "…+N 行 · GitHub で開く ↗"
9500
9515
  // footer (only linking to GitHub when a PR exists). Mirrors the flagship artifact's
9501
9516
  // ghDiffHtml so the app pixel-matches. `more` = extra unshown line count; `pr` = url.
9517
+ // Request fold threshold (Masa 2026-07-27): Request is supposed to be the user's short
9518
+ // ask (spec §4), but a mispaste — e.g. another card's whole content (Request/Result/
9519
+ // Activity) copied in — can make it huge and invisible in the single-line reply input
9520
+ // it came from. Anything past this size folds behind the same `.lacc` pulldown used by
9521
+ // Activity/Conversation instead of the card growing unbounded; a normal short ask stays
9522
+ // under the threshold and never shows the control. Length OR newline-count catches both
9523
+ // "one giant line" and "many lines" (goal.text is whitespace-collapsed before it reaches
9524
+ // the client, so in practice the length check is what fires; the newline check guards
9525
+ // against a future caller that preserves them).
9526
+ const REQUEST_FOLD_OVER = 220;
9527
+ // How many characters of a folded Request sit next to the closed `›` (spec §3's "first
9528
+ // line") — goal.text has no real line breaks by the time it's client-side (whitespace
9529
+ // is collapsed to single spaces), so this is a character peek standing in for "line 1".
9530
+ const REQUEST_PEEK_CHARS = 140;
9531
+ function shouldFoldRequestText(text){
9532
+ const t = String(text || '');
9533
+ return t.length > REQUEST_FOLD_OVER || (t.match(/\n/g) || []).length >= 2;
9534
+ }
9502
9535
  const SA_DIFF_FOLD_OVER = 14;
9503
9536
  function saGhDiffRowsHtml(rows, more, pr, goalId){
9504
9537
  if (!rows || !rows.length) return '';
@@ -9644,11 +9677,19 @@ function saItemHtml(it, i){
9644
9677
  // §1.5-3: with Open it moved into the body, a card with no PR / tokens / tests has no
9645
9678
  // chips at all — render no meta row rather than an empty one holding open a gap.
9646
9679
  const meta = chips.length ? `<div class="rmeta">${chips.join('')}</div>${metaDetail}` : '';
9647
- // ── Request (spec §4): a PLAIN section — the user's own short words (goal.text). Never
9648
- // an accordion, never the implementation spec (that lives in Activity below). ──
9649
- const reqSec = it.goalText
9650
- ? `<section class="rsec rreq"><div class="rsec-h">Request</div><p class="rbody rask">${esc(it.goalText)}</p></section>`
9651
- : '';
9680
+ // ── Request (spec §4): a PLAIN section — the user's own short words (goal.text), never
9681
+ // the implementation spec (that lives in Activity below). A normal short ask is never an
9682
+ // accordion; a mispasted giant Request instead folds using the SAME `.lacc` pulldown as
9683
+ // Activity/Conversation below (spec §3: chevron on the first line, offset-indented body,
9684
+ // no left rule) — not a bespoke widget (Masa 2026-07-27: "他の›と同じルールじゃなくない?").
9685
+ // §4 assumed Request was always short, which a mispaste can break; this reuses the one
9686
+ // pulldown shape the card already has rather than inventing a second one. ──
9687
+ const reqFold = it.goalText && shouldFoldRequestText(it.goalText);
9688
+ const reqOpen = reqFold && SA.detOpen.has(`${gk}:req`);
9689
+ const reqPeek = reqFold ? `${it.goalText.slice(0, REQUEST_PEEK_CHARS).trimEnd()}…` : '';
9690
+ const reqSec = !it.goalText ? '' : reqFold
9691
+ ? `<section class="rsec rreq"><div class="rsec-h">Request</div><details class="lacc" data-detkey="${gk}:req"${reqOpen ? ' open' : ''}><summary><i class="ltog">›</i><p class="rbody rask lfirst">${esc(reqPeek)}</p></summary><div class="lbody"><p class="rbody rask">${esc(it.goalText)}</p></div></details></section>`
9692
+ : `<section class="rsec rreq"><div class="rsec-h">Request</div><p class="rbody rask">${esc(it.goalText)}</p></section>`;
9652
9693
  // ── Open it (Masa 2026-07-22): a plain section in the card's own typography — the same
9653
9694
  // heading + body shape as Request and Result, not a chip in another vocabulary. The
9654
9695
  // address the worker declared IS the control (press it → the process starts on demand →
@@ -10289,6 +10330,8 @@ $('seeall').addEventListener('click', async (e) => {
10289
10330
  if (label) label.innerHTML = open ? 'Collapse' : `Show all <span class="n">${diffMore.dataset.total}</span> lines`;
10290
10331
  return;
10291
10332
  }
10333
+ // (A folded Request's toggle needs no handler here — it's a `[data-detkey]` `.lacc`
10334
+ // <details>, same as Activity/Conversation, wired generically at render time.)
10292
10335
  const removeImage = e.target.closest('[data-saarm]');
10293
10336
  if (removeImage) {
10294
10337
  const [goalId, index] = removeImage.dataset.saarm.split(':').map(Number);
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.74",
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": {