@galda/cli 0.10.92 → 0.10.94

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.
@@ -1,5 +1,39 @@
1
- const CHANGE_REQUEST_RE = /(?:修正|直して|解決して|改善して|一致させ|統一して|実装して|追加して|削除して|変更して|作って|fix\b|solve\b|resolve\b|implement\b|change\b|make\b)/i;
2
- const REFUSAL_RESULT_RE = /(?:直せません|修正できません|解決できません|実現できません|不可能です|cannot\s+(?:be\s+)?(?:fix(?:ed)?|solv(?:e|ed)|implement(?:ed)?|done)|can't\s+(?:fix|solve|implement|do)|unable\s+to\s+(?:fix|solve|implement))/i;
1
+ // Which user requests are "change requests" whose ONLY acceptable outcome is the
2
+ // change itself. Deliberately wide on Japanese request forms: the first cut only
3
+ // matched the 「〜して」 imperative stem, so ordinary asks like 「〜してほしい」
4
+ // 「〜できる?」「〜消せますか?」「〜にして」 never qualified and their refusals
5
+ // sailed straight into Review as completed work (2026-07-29 audit).
6
+ const CHANGE_REQUEST_RE = new RegExp([
7
+ '修正', '直して', '直せ', 'なおして', '解決', '改善', '一致させ', '統一',
8
+ '実装', '追加', '削除', '変更', '作って', '対応して', '消して', '消せ',
9
+ '出して', '表示して', '止めて', '揃え', '直したい', '外して', '戻して',
10
+ 'できるように', 'にして', 'して(?:ほしい|下さい|ください)', 'られるように',
11
+ '\\bfix\\b', '\\bsolve\\b', '\\bresolve\\b', '\\bimplement\\b', '\\bchange\\b',
12
+ '\\bmake\\b', '\\bremove\\b', '\\badd\\b', '\\bhide\\b', '\\bshow\\b',
13
+ ].join('|'), 'i');
14
+
15
+ // What counts as "the agent closed the request by declaring it impossible".
16
+ // The earlier list missed the exact phrasings Masa reported —「仕様なので
17
+ // 変えられません」「対応できません」— plus the English "by design / working as
18
+ // intended" family, which is the same move in a different register.
19
+ const REFUSAL_RESULT_RE = new RegExp([
20
+ '直せません', '直せない', '修正できません', '修正できない',
21
+ '解決できません', '解決できない', '実現できません', '実現できない',
22
+ '変えられません', '変えられない', '変更できません', '変更できない',
23
+ '対応できません', '対応できない', '削除できません', '消せません',
24
+ '実装できません', '実装できない', '不可能です', '仕様(?:のため|なので|上)[^。\\n]{0,20}(?:できません|変えられません|変更できません)',
25
+ 'cannot\\s+(?:be\\s+)?(?:fix(?:ed)?|solv(?:e|ed)|implement(?:ed)?|chang(?:e|ed)|remov(?:e|ed)|done)',
26
+ "can't\\s+(?:fix|solve|implement|change|remove|do)",
27
+ 'unable\\s+to\\s+(?:fix|solve|implement|change|remove)',
28
+ 'not\\s+possible\\s+to\\s+(?:fix|change|implement|remove)',
29
+ 'by\\s+design[^.\\n]{0,30}(?:cannot|can\'t|won\'t)',
30
+ 'working\\s+as\\s+intended',
31
+ ].join('|'), 'i');
32
+
33
+ // An explicit, resolved answer beats the refusal phrase: a worker that says
34
+ // "直せませんでしたが、代わりに X を実装しました" DID deliver. Only the shapes
35
+ // where the refusal is the whole outcome should be blocked.
36
+ const RESOLVED_ANYWAY_RE = /(?:代わりに|その代わり|instead[, ]|workaround|回避策)/i;
3
37
 
4
38
  export const COMPLETION_CONTRACT_PROMPT = [
5
39
  'MANAGER COMPLETION CONTRACT:',
@@ -8,10 +42,14 @@ export const COMPLETION_CONTRACT_PROMPT = [
8
42
  '- Do not report a change request as complete with “cannot fix”, “直せません”, or equivalent. If a real external blocker remains, state the concrete blocker and leave the work explicitly unfinished instead of presenting it for Review as completed.',
9
43
  ].join('\n');
10
44
 
11
- export function completionContractIssue({ request = '', followUp = '', result = '', status = '' } = {}) {
45
+ export function completionContractIssue({ request = '', followUp = '', result = '', status = '', changedFiles = [] } = {}) {
12
46
  if (status !== 'done') return null;
13
47
  const requested = `${request}\n${followUp}`;
14
- if (!CHANGE_REQUEST_RE.test(requested) || !REFUSAL_RESULT_RE.test(String(result))) return null;
48
+ const text = String(result);
49
+ if (!CHANGE_REQUEST_RE.test(requested) || !REFUSAL_RESULT_RE.test(text)) return null;
50
+ // Evidence beats phrasing, in both directions: if the run actually changed
51
+ // files, or explicitly delivered an alternative, it is not a closed door.
52
+ if ((changedFiles ?? []).length > 0 || RESOLVED_ANYWAY_RE.test(text)) return null;
15
53
  return {
16
54
  kind: 'unresolved-refusal',
17
55
  reason: '変更・解決依頼に対して「対応不能」で終了しており、要求した結果が実現されていません。',
package/engine/lib.mjs CHANGED
@@ -1346,9 +1346,18 @@ export function looksIncomplete(text) {
1346
1346
  const t = String(text ?? '').trim();
1347
1347
  if (!t) return true;
1348
1348
  if (t.length >= 200) return false;
1349
- // A short blurb ending mid-word/mid-clause (no sentence-final punctuation
1350
- // in any of ja/en) reads as truncated, not as a deliberate one-liner.
1351
- if (!/[.!?。!?」))]$/.test(t)) return true;
1349
+ // A short blurb ending mid-clause reads as truncated. This used to be "no
1350
+ // sentence-final punctuation truncated", which was far too wide: a terse
1351
+ // but FINISHED one-liner ("worker done", "対応済み") has no period either, and
1352
+ // that false positive marked the task `interrupted` → the whole goal `partial`,
1353
+ // so a legitimate 0-file answer/investigation goal never reached Review
1354
+ // (2026-07-29 audit; it is also what deterministically broke
1355
+ // auto-test-rework.test.mjs). Require an actual DANGLING tail instead: a
1356
+ // trailing connector/particle, a comma/colon/opening bracket, or a hyphen
1357
+ // left mid-word. The fragment this rule was written for (goal-783/task-448)
1358
+ // is still caught — by the future-intent opener below, which is the real
1359
+ // signal there.
1360
+ if (!/[.!?。!?」))]$/.test(t) && /(?:[,、::\-—((「【]|\b(?:and|but|or|so|then|to|the|a|an|of|for|with|because|that|which|while|after|before|from|into)|[でをにがはとしてやりつつ])$/i.test(t)) return true;
1352
1361
  // goal-783/task-448's actual fragment ("I'll wait for this background
1353
1362
  // full-suite run to complete rather than poll.") IS a grammatically
1354
1363
  // complete sentence, so the check above misses it — but it's a one-shot
@@ -2477,59 +2486,14 @@ export function dismissGoal({ goalStatus, outcome = 'continue' } = {}) {
2477
2486
  // remains unsure. Returning unsure is safe because the UI already presents
2478
2487
  // these exact choices without moving the goal.
2479
2488
  export const REPLY_INTENTS = [...REPLY_OUTCOMES, 'unsure'];
2480
- export function buildReplyIntentPrompt({ goalText, reply, currentProject = null, projects = [] } = {}) {
2481
- const projectLines = (projects ?? []).map((project) =>
2482
- `- ${project.id}: ${project.name || project.id} | root=${project.dir || '(unknown)'} | scope=${project.note || '(not specified)'}`);
2483
- return [
2484
- '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.',
2485
- 'Decide what the reply ASKS FOR exactly one of:',
2486
- '- "continue": fix or change the work that was just done.',
2487
- '- "rescope": stop and rethink; the request itself should now say something different.',
2488
- '- "split": a separate piece of work that should not touch this one.',
2489
- '- "answer": a question about the work. Nothing should be built or changed.',
2490
- '- "unsure": you genuinely cannot tell. Prefer this over guessing.',
2491
- 'Judge the full meaning. A question followed by a requested change is "continue".',
2492
- 'Also decide which project must receive the requested work.',
2493
- '- Keep targetProjectId equal to the current project for ordinary fixes to the reviewed work.',
2494
- '- If the current project is design/docs/prototype-only and the reply asks to implement or launch the chosen design in the real product, select the matching production/code project.',
2495
- '- Never infer a project that is not listed. If the target is ambiguous, use outcome "unsure" and targetProjectId null.',
2496
- 'Output ONLY one line of raw JSON: {"outcome":"continue|rescope|split|answer|unsure","targetProjectId":"listed-id-or-null"}.',
2497
- '',
2498
- `Current project: ${currentProject?.id || '(unknown)'}`,
2499
- 'Available projects:',
2500
- ...(projectLines.length ? projectLines : ['- (none)']),
2501
- '',
2502
- 'The request that was worked on:',
2503
- String(goalText ?? '').slice(0, 2000),
2504
- '',
2505
- 'Their reply:',
2506
- String(reply ?? '').slice(0, 4000),
2507
- ].join('\n');
2508
- }
2509
-
2510
- export function parseReplyIntent(raw) {
2511
- const unread = { outcome: 'unsure', read: false };
2512
- if (!raw || typeof raw !== 'string') return unread;
2513
- const m = raw.match(/\{[\s\S]*\}/);
2514
- if (!m) return unread;
2515
- let obj;
2516
- try { obj = JSON.parse(m[0]); } catch { return unread; }
2517
- if (!REPLY_INTENTS.includes(obj?.outcome)) return unread;
2518
- const targetProjectId = typeof obj.targetProjectId === 'string' && obj.targetProjectId.trim()
2519
- ? obj.targetProjectId.trim() : null;
2520
- return { outcome: obj.outcome, targetProjectId, read: true };
2521
- }
2522
-
2523
- // 宛先(docs/design/ONE-CONVERSATION.md §1.2)
2524
- //
2525
- // チャットで「#12 の件だけど」と書けたら、そのチケット宛て。**決定論で、我々の推測は
2526
- // ゼロ**。これが要るのは、今まで「同じ話かどうか」を我々の層が文字列の似かた(0.72)で
2527
- // 当てずっぽうに決めていたから——当たれば黙って合流し、外れれば別のカードが生えた。
2528
- // 人が番号を書けるなら、推測する理由が無い。
2529
- //
2530
- // 最初の `#N` だけを見る。番号が実在しなければ宛先なしとして扱う(=ふつうに新しい
2531
- // チケットになる)ので、`#` を含むだけの文章がどこかへ黙って吸い込まれることはない。
2532
- // 本文は削らない: 言われた言葉をそのまま作業AIへ渡す(P0 の「原文100%」と同じ約束)。
2489
+ // buildReplyIntentPrompt / parseReplyIntent lived here: the prompt and parser for a
2490
+ // utility LLM that decided what a Review reply MEANT before the goal's own agent saw
2491
+ // it. They are gone with their caller. Its "answer" bucket "a question about the
2492
+ // work. Nothing should be built or changed" — swallowed every politely-phrased
2493
+ // Japanese instruction, because there is no other way to ask politely in Japanese.
2494
+ // Claude Code and Codex already tell a question from an instruction; a second opinion
2495
+ // in front of them could only ever be worse-informed. See the `auto` branch of
2496
+ // /dismiss in engine/server.mjs and engine/test/reply-goes-to-the-agent.test.mjs.
2533
2497
  export function parseGoalAddress(text) {
2534
2498
  // `(?![\w-])` =「番号の直後に英数字やハイフンが続くなら、それは番号ではない」:
2535
2499
  // `#1f6f4a`(色) や `#12-3` を宛先と読み違えない。日本語は \w に入らないので
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, parseRequestedWorkspaceDir } from './lib.mjs';
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, parseRunDeclaration, SHOT_DIR, collectShotNames, parseGoalAddress, REPLY_OUTCOMES, buildReplyIntentPrompt, parseReplyIntent, buildGoalMessage, serializeGoalMessage, parseGoalMessages, computeInternalQualityMetrics, classifyInternalQualityTaskError, resolveWorkspaceMode, goalSessionFor, setGoalAgentSession, switchGoalAgentSession, parseApprovalRequest } from './lib.mjs';
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, parseRunDeclaration, SHOT_DIR, collectShotNames, parseGoalAddress, REPLY_OUTCOMES, buildGoalMessage, serializeGoalMessage, parseGoalMessages, computeInternalQualityMetrics, classifyInternalQualityTaskError, resolveWorkspaceMode, goalSessionFor, setGoalAgentSession, switchGoalAgentSession, parseApprovalRequest } from './lib.mjs';
25
25
  import { createSerialQueue } from './lib.mjs';
26
26
  import { parseRequirementEvidence, unmappedChangedFiles } from './lib.mjs';
27
27
  import { isPrClosed } from './lib.mjs';
@@ -2813,6 +2813,7 @@ async function runTask(task) {
2813
2813
  followUp: task.detail,
2814
2814
  result: task.result,
2815
2815
  status: task.status,
2816
+ changedFiles: task.changedFiles,
2816
2817
  });
2817
2818
  if (completionIssue) {
2818
2819
  task.status = 'interrupted';
@@ -3047,6 +3048,7 @@ async function runTask(task) {
3047
3048
  followUp: task.reply ? task.detail : '',
3048
3049
  result: task.result,
3049
3050
  status: task.status,
3051
+ changedFiles: task.changedFiles,
3050
3052
  });
3051
3053
  if (completionIssue) {
3052
3054
  task.status = 'interrupted';
@@ -3105,13 +3107,21 @@ function testEnvironmentPreflight(dir, dependencySourceDir = null) {
3105
3107
  const sources = nodePath ? nodePath.split(process.platform === 'win32' ? ';' : ':') : [];
3106
3108
  const hasPuppeteer = sources.some((p) => existsSync(join(p, 'puppeteer-core', 'package.json')));
3107
3109
  const npm = spawnSync('npm', ['--version'], { encoding: 'utf8' });
3110
+ // BLOCKING vs ADVISORY (2026-07-29, docs/INCIDENT-2026-07-25-29-INTERFACE-AUDIT.md).
3111
+ // Only `node`/`npm` make `npm test` impossible to START. `dependencies` and
3112
+ // `puppeteer` are properties of THIS repo's own browser suite — most user
3113
+ // projects legitimately have neither, and treating them as blocking meant
3114
+ // runTestsOnce returned "0 failed" for a suite it never ran, which
3115
+ // runProjectTests then published as ok:true. A missing puppeteer-core is
3116
+ // already discounted downstream by countInfraFlakes, so the honest thing is
3117
+ // to run the suite and report the advisory alongside the real result.
3108
3118
  const checks = [
3109
- { id: 'node', ok: Boolean(process.execPath), detail: process.version },
3110
- { id: 'npm', ok: npm.status === 0, detail: npm.status === 0 ? npm.stdout.trim() : 'npm command unavailable' },
3111
- { id: 'dependencies', ok: sources.length > 0, detail: sources.length ? `${sources.length} node_modules source(s)` : 'node_modules not found' },
3112
- { id: 'puppeteer', ok: hasPuppeteer, detail: hasPuppeteer ? 'puppeteer-core available' : 'puppeteer-core missing; run npm install in the project directory' },
3119
+ { id: 'node', ok: Boolean(process.execPath), detail: process.version, blocking: true },
3120
+ { id: 'npm', ok: npm.status === 0, detail: npm.status === 0 ? npm.stdout.trim() : 'npm command unavailable', blocking: true },
3121
+ { id: 'dependencies', ok: sources.length > 0, detail: sources.length ? `${sources.length} node_modules source(s)` : 'node_modules not found', blocking: false },
3122
+ { id: 'puppeteer', ok: hasPuppeteer, detail: hasPuppeteer ? 'puppeteer-core available' : 'puppeteer-core missing; run npm install in the project directory', blocking: false },
3113
3123
  ];
3114
- return { ok: checks.every((c) => c.ok), checks };
3124
+ return { ok: checks.every((c) => c.ok), canRun: checks.every((c) => c.ok || !c.blocking), checks };
3115
3125
  }
3116
3126
  function runTestsOnce(dir, dependencySourceDir = null) {
3117
3127
  // The timeout is env-overridable so tests can force the killed-run path fast;
@@ -3119,7 +3129,12 @@ function runTestsOnce(dir, dependencySourceDir = null) {
3119
3129
  const runTimeout = Number(process.env.MANAGER_TEST_TIMEOUT_MS) || 240000;
3120
3130
  return testRunQueue.run(() => new Promise((res) => {
3121
3131
  const preflight = testEnvironmentPreflight(dir, dependencySourceDir);
3122
- if (!preflight.ok) return res({ passed: 0, failed: 0, preflight, timedOut: false, realAssertion: false, infraOnly: true, infraFlakes: 0, detail: preflight.checks.filter((c) => !c.ok).map((c) => c.detail).join('\n'), failingNames: [] });
3132
+ // Never report an unrun suite as a run with zero failures runProjectTests
3133
+ // turns `failed === 0` into ok:true, and that is the shape a caller reads as
3134
+ // "verification passed". A suite that could not START is `notRun`.
3135
+ if (!preflight.canRun) {
3136
+ return res({ notRun: true, passed: 0, failed: 0, preflight, timedOut: false, realAssertion: false, infraOnly: true, infraFlakes: 0, detail: preflight.checks.filter((c) => !c.ok && c.blocking).map((c) => c.detail).join('\n'), failingNames: [] });
3137
+ }
3123
3138
  const nodePath = testNodePath(dir, dependencySourceDir);
3124
3139
  execFile('npm', ['test'], { cwd: dir, timeout: runTimeout, env: { ...process.env, ...(nodePath ? { NODE_PATH: nodePath } : {}) }, maxBuffer: 8 * 1024 * 1024 }, (e, out = '', err = '') => {
3125
3140
  const text = `${out}\n${err}`;
@@ -3158,6 +3173,12 @@ async function runProjectTests(dir, dependencySourceDir = null) {
3158
3173
  try { hasTest = !!JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')).scripts?.test; } catch { /* no package.json */ }
3159
3174
  if (!hasTest) return { ran: false };
3160
3175
  let r = await runTestsOnce(dir, dependencySourceDir);
3176
+ // The suite could not be started at all (node/npm missing). Report it as
3177
+ // NOT RUN — the same honest shape as the documentation-only / gate-off skips
3178
+ // — instead of ran:true + ok:true, which reads as "verification passed".
3179
+ if (r.notRun) {
3180
+ return { ran: false, skipped: true, skippedReason: `verification preflight blocked: ${r.detail || 'test environment unavailable'}`, preflight: r.preflight ?? null, detail: r.detail ?? '' };
3181
+ }
3161
3182
  // Flake resistance: the integration suite spawns real servers; under load a
3162
3183
  // startup can time out. Re-run once and take the fewer failures — a real
3163
3184
  // failure fails both runs, a flake clears on the retry — so a good goal is
@@ -3701,10 +3722,17 @@ async function verifyGate(goal, prProject, siblings, baseProject, activityTask =
3701
3722
  if (goal.testResult.ran && goal.testResult.infraOnly && !goal.testResult.inconclusive) {
3702
3723
  goalAct(`Project tests: all ${goal.testResult.infraFlakes || goal.testResult.failed || ''} failure(s) were infra flakes (a real server could not boot / a wait timed out under machine load), not code regressions — not blocking. Re-run when the machine is idle to confirm.`.replace(/\s{2,}/g, ' '));
3703
3724
  }
3704
- if (goal.testResult.ran && goal.testResult.preflight && !goal.testResult.preflight.ok) {
3725
+ // Preflight is an ADVISORY note, not a verdict (2026-07-29). It used to force
3726
+ // inconclusive=true, which silently disabled the gate for every project that
3727
+ // lacks this repo's puppeteer suite — i.e. essentially every user project —
3728
+ // while still publishing testResult.ok === true. Now: say what is missing, and
3729
+ // let the REAL run decide. A suite that could not start at all never reaches
3730
+ // here (runProjectTests returns ran:false with skippedReason instead).
3731
+ if (goal.testResult.preflight && !goal.testResult.preflight.ok) {
3705
3732
  const missing = goal.testResult.preflight.checks.filter((c) => !c.ok).map((c) => c.id).join(', ');
3706
- goal.testResult.inconclusive = true;
3707
- goalAct(`Project tests were not started: verification preflight failed (${missing}). This is an environment issue, not an implementation failure. Fix the listed dependency or tool, then re-run verification.`);
3733
+ goalAct(goal.testResult.ran
3734
+ ? `Verification environment note: ${missing} missing. The suite still ran; browser-dependent tests in it may fail for environment reasons (those are discounted as infra flakes).`
3735
+ : `Project tests were not started: verification preflight failed (${missing}). This is an environment issue, not an implementation failure. Fix the listed dependency or tool, then re-run verification.`);
3708
3736
  }
3709
3737
  // Baseline diff (Masa 2026-07-19): block ONLY on failures THIS change
3710
3738
  // introduced. A test already red on the base — e.g. persist-failure on clean
@@ -5494,29 +5522,13 @@ const server = createServer(async (req, res) => {
5494
5522
  // 「聞いてるだけ」への返事 (ONE-CONVERSATION.md §1.3 answer)。
5495
5523
  //
5496
5524
  // 答えるのは**作業した AI 本人**=その agent の session を resume する。状況一覧しか
5497
- // Resolve the app's `outcome:auto` contract before changing goal state. The
5498
- // utility call is classification-only; the goal's own agent/session still
5499
- // writes an answer or performs the requested change. If routing is unavailable,
5500
- // return unsure so a customer chooses explicitly instead of accidentally
5501
- // starting a ten-minute implementation worker for a question.
5502
- async function judgeReplyIntent(goal, reply) {
5503
- try {
5504
- const currentProject = projects.find((project) => project.id === goal.projectId) ?? null;
5505
- const r = await runUtility({
5506
- prompt: buildReplyIntentPrompt({ goalText: goal.text, reply, currentProject, projects }),
5507
- cwd: ROOT,
5508
- tools: 'Read',
5509
- });
5510
- const parsed = parseReplyIntent(r.result);
5511
- if (!parsed.read) return { outcome: 'unsure', targetProjectId: null };
5512
- if (parsed.targetProjectId && !projects.some((project) => project.id === parsed.targetProjectId)) {
5513
- return { outcome: 'unsure', targetProjectId: null };
5514
- }
5515
- return { outcome: parsed.outcome, targetProjectId: parsed.targetProjectId };
5516
- } catch {
5517
- return { outcome: 'unsure', targetProjectId: null };
5518
- }
5519
- }
5525
+ // judgeReplyIntent lived here: a utility-LLM pass that sorted a Review reply into
5526
+ // continue / rescope / split / answer BEFORE the goal's own agent saw it. It is
5527
+ // gone see the `auto` branch of /dismiss for the measured reason. Its stated
5528
+ // worry was "accidentally starting a ten-minute implementation worker for a
5529
+ // question", but the trade it actually made was the opposite one: it answered
5530
+ // real instructions, and the person paid several round trips to get the work
5531
+ // started at all. Deciding that is the agent's job.
5520
5532
 
5521
5533
  // 知らない別AIでは「なぜそうしたか」に答えられない(Masa確定 2026-07-22)。
5522
5534
  // コードは1文字も変わらない: ツールは Read だけ、permissionMode は 'plan'
@@ -5580,16 +5592,33 @@ async function answerGoalQuestion(goal, question) {
5580
5592
 
5581
5593
  if (auto) {
5582
5594
  if (!text) return json(res, 400, { error: 'text required' });
5583
- const routed = await judgeReplyIntent(goal, text);
5584
- outcome = routed.outcome;
5585
- requested.targetProjectId = routed.targetProjectId;
5586
- if (outcome === 'unsure') return json(res, 200, {
5587
- goal,
5588
- outcome,
5589
- choices: REPLY_OUTCOMES,
5590
- projectChoices: projects.map((project) => ({ id: project.id, name: project.name, current: project.id === goal.projectId })),
5591
- });
5592
- if (requested.targetProjectId && requested.targetProjectId !== goal.projectId) outcome = 'split';
5595
+ // The reply goes to the agent exactly as written. Galda does not decide
5596
+ // first what the person meant.
5597
+ //
5598
+ // This used to call a utility LLM (judgeReplyIntent) to sort the reply
5599
+ // into continue / rescope / split / answer before the agent ever saw it.
5600
+ // Its definition of "answer" was "a question about the work. Nothing
5601
+ // should be built or changed" — and Japanese has no other way to make a
5602
+ // polite request. Measured on the operator's own instance 2026-07-25..29:
5603
+ // 「案3で実装出来ますか?」「Aの方針で実装できますか!?」「あれ、prいるよね?」
5604
+ // 「okなんでprなげれますか?」「案がみえないのでローカルで確認できるように貰える?」
5605
+ // were all real instructions, and every one of them could be read as a
5606
+ // question. When it read one that way it created NO task at all, so the
5607
+ // request was answered instead of done and the person had to ask two or
5608
+ // three more times.
5609
+ //
5610
+ // The fix is not a better classifier. Claude Code and Codex already
5611
+ // decide correctly whether a message wants an answer or a change — that
5612
+ // is what they are. Galda's job is the interface around them, not a
5613
+ // second opinion in front of them. Note the `answer` route spawned an
5614
+ // agent too (Read-only, plan mode), so passing the reply through costs
5615
+ // no extra run — it removes one utility-LLM call per reply.
5616
+ //
5617
+ // Explicit routes are untouched: when the PERSON picks rescope / split /
5618
+ // answer from the chips, `requested.outcome` carries it and this branch
5619
+ // never runs. A human choosing where their words go is the interface
5620
+ // working; Galda guessing is not.
5621
+ outcome = 'continue';
5593
5622
  }
5594
5623
  // 返事の行き先4種 (ONE-CONVERSATION.md §1.3). 既定は今までどおり continue なので、
5595
5624
  // outcome を送ってこない今の UI の挙動は1ミリも変わらない。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@galda/cli",
3
- "version": "0.10.92",
3
+ "version": "0.10.94",
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": {