@galda/cli 0.10.91 → 0.10.93

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
@@ -2987,12 +2996,14 @@ export function replayQueueLog(rawText, reviewDefinitions = {}) {
2987
2996
  }
2988
2997
  for (const t of tasks) {
2989
2998
  if (t.status !== 'running') continue;
2999
+ const parent = goals.find((g) => g.id === t.goalId);
2990
3000
  // A Manager restart is an infrastructure interruption, not a request for
2991
3001
  // human judgment. Resume the same task once from the files/session already
2992
- // on disk. The bounded counter prevents a crash-looping task from being
2993
- // relaunched forever; a second interrupted boot remains visibly retryable.
3002
+ // on disk, but only while its parent goal is genuinely in flight. A stale
3003
+ // running task under a partial/review/done goal must never resurrect work
3004
+ // the customer has already moved past. The bounded counter prevents loops.
2994
3005
  const recoveries = Number(t.restartRecoveries ?? 0);
2995
- if (recoveries < 1) {
3006
+ if (parent?.status === 'running' && recoveries < 1) {
2996
3007
  t.status = 'queued';
2997
3008
  t.restartRecoveries = recoveries + 1;
2998
3009
  t.recoveredFromRestart = true;
package/engine/server.mjs CHANGED
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@galda/cli",
3
- "version": "0.10.91",
3
+ "version": "0.10.93",
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": {