@galda/cli 0.10.83 → 0.10.84

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
@@ -5368,7 +5368,10 @@ function renderReviewChecklist(){
5368
5368
  // own test result — so a reviewer can judge at a glance, before scrolling.
5369
5369
  const grs = goal.reviewSummary ?? {};
5370
5370
  const gtr = goal.testResult;
5371
- const testLine = gtr?.ran ? `<span class="tbadge ${gtr.ok ? 'ok' : 'no'}">tests ${gtr.passed}✓${gtr.failed ? ` / ${gtr.failed}✗` : ''}</span>` : '';
5371
+ const testPreflightFailed = gtr?.preflight && !gtr.preflight.ok;
5372
+ const testLine = gtr?.ran ? (testPreflightFailed
5373
+ ? `<span class="tbadge">tests need environment setup</span>`
5374
+ : `<span class="tbadge ${gtr.ok ? 'ok' : 'no'}">tests ${gtr.passed}✓${gtr.failed ? ` / ${gtr.failed}✗` : ''}</span>`) : '';
5372
5375
  // #377-7 (Masa, recurring): the base UI is ALWAYS English — Japanese is a
5373
5376
  // per-project CONTENT customization, never chrome. So the fixed system labels
5374
5377
  // below stay English regardless of reviewDefinition.language; only the
@@ -5398,7 +5401,7 @@ function renderReviewChecklist(){
5398
5401
  vchips.push(`<span class="vchip none">${esc(workspaceModeText(goal))}</span>`);
5399
5402
  if (goal.workspaceWarning) vchips.push(`<span class="vchip human high">⚠ ${esc(goal.workspaceWarning.slice(0, 80))}</span>`);
5400
5403
  vchips.push(gtr?.ran
5401
- ? (gtr.ok ? `<span class="vchip ok">✓ ${VF.pass}</span>` : `<span class="vchip no">✗ ${VF.fail}</span>`)
5404
+ ? (testPreflightFailed ? `<span class="vchip human">! tests need setup</span>` : gtr.ok ? `<span class="vchip ok">✓ ${VF.pass}</span>` : `<span class="vchip no">✗ ${VF.fail}</span>`)
5402
5405
  : `<span class="vchip none">— ${VF.untested}</span>`);
5403
5406
  vchips.push(hasProof ? `<span class="vchip ok">✓ ${VF.proof}</span>` : `<span class="vchip none">— ${VF.noproof}</span>`);
5404
5407
  if (risk?.shouldPause) vchips.push(`<span class="vchip human high">⚠ ${VF.human}</span>`);
@@ -11747,15 +11750,23 @@ function gearSetHTML(){
11747
11750
  ? acct + `<div class="srow"><div class="sl">Plan</div><span class="bx-pdot"><i></i>Paid</span></div>
11748
11751
  <div class="srow"><div class="sl">Billing<span class="sd">manage or cancel in Stripe</span></div><button class="bx-cchip" data-gportal="1">Manage</button></div>`
11749
11752
  : acct + freeUsage;
11753
+ const st = state.stability;
11754
+ const stability = st ? `<div class="srow"><div class="sl">Verification health<span class="sd">Last 24h · implementation ${st.implementationFailures} · environment ${st.verificationInfrastructure} · inconclusive ${st.timeoutsOrInconclusive} · worker holds ${st.workerHolds}</span></div><button class="optchip" data-gstability="1">Refresh</button></div>` : `<div class="srow"><div class="sl">Verification health<span class="sd">Loading…</span></div></div>`;
11750
11755
  return `
11751
11756
  <div class="srow"><div class="sl">Language<span class="sd">Review checklist language for this project</span></div>
11752
11757
  <button class="optchip${lang === 'auto' ? ' on' : ''}" data-gslang="auto">Auto</button>
11753
11758
  <button class="optchip${lang === 'en' ? ' on' : ''}" data-gslang="en">EN</button>
11754
11759
  <button class="optchip${lang === 'ja' ? ' on' : ''}" data-gslang="ja">JA</button></div>
11755
11760
  ${billingRow}
11761
+ ${stability}
11756
11762
  ${gearRulesHTML()}
11757
11763
  <div class="srow dis"><div class="sl">Data folder<span class="sd">Runs are stored in the engine's chat-runs folder on the host</span></div></div>`;
11758
11764
  }
11765
+ async function loadStability(){
11766
+ try { const r = await fetch(withKey('/api/stability')); state.stability = r.ok ? await r.json() : null; }
11767
+ catch { state.stability = null; }
11768
+ if ($('gearPop') && custTab === 'settings') buildGearPop();
11769
+ }
11759
11770
  // ONE-CONVERSATION §1.5a: the project's working rules — the file(s) the connected
11760
11771
  // AI actually reads (Claude Code → CLAUDE.md, Codex → AGENTS.md). Read-only: Galda
11761
11772
  // does NOT own these rules, it only surfaces them (editing is done in an editor or
@@ -11844,7 +11855,10 @@ function buildGearPop(){
11844
11855
  };
11845
11856
  const rulesBtn = pop.querySelector('[data-grules]');
11846
11857
  if (rulesBtn) rulesBtn.onclick = () => { state.rulesOpen = !state.rulesOpen; buildGearPop(); };
11858
+ const stabilityBtn = pop.querySelector('[data-gstability]');
11859
+ if (stabilityBtn) stabilityBtn.onclick = () => { state.stability = null; buildGearPop(); loadStability(); };
11847
11860
  loadProjectRules();
11861
+ if (!state.stability) loadStability();
11848
11862
  return;
11849
11863
  }
11850
11864
  if (custTab === 'mcp') {
package/engine/lib.mjs CHANGED
@@ -1474,7 +1474,7 @@ export function buildGoalPrBody({ lang, goal, goalTasks, files, owner, branch, p
1474
1474
  // 修正/追加/改善/調査 so an "追加したい" request never becomes "…の修正", and (c) a
1475
1475
  // failed/missing reply falls back to goal.text (no regression) — all without an
1476
1476
  // LLM call. The app's saReviewTitle reads reviewSummary.title first (PR-B).
1477
- export function buildReviewSummaryPrompt({ lang, goalText, changed, reports }) {
1477
+ export function buildReviewSummaryPrompt({ lang, goalText, changed, reports, latestFeedback = '' }) {
1478
1478
  return [
1479
1479
  `あるゴールの実装が完了しました。レビューする人が一目で判断できるよう、やさしい${lang === 'ja' ? '日本語' : lang}で書いてください。`,
1480
1480
  '専門用語・長い説明は禁止。小学生でも分かる短い言い方。JSONのみ出力(前後に文章を書かない):',
@@ -1483,6 +1483,7 @@ export function buildReviewSummaryPrompt({ lang, goalText, changed, reports }) {
1483
1483
  + '例:「Xの不具合」→「Xの修正」/「Yを追加したい」→「Yの追加」/「Zを速くしたい」→「Zの改善」/「なぜWか調べて」→「Wの調査」。',
1484
1484
  '',
1485
1485
  `ゴール: ${(goalText ?? '').slice(0, 300)}`,
1486
+ latestFeedback ? `最新のユーザー返信(この再レビューで満たすべき条件。初回の結果ではなく、これへの対応をchanged/checkに書く):\n${String(latestFeedback).slice(0, 1000)}` : null,
1486
1487
  `変更ファイル: ${(changed ?? []).join(', ') || '(なし)'}`,
1487
1488
  `ワーカー報告:\n${(reports ?? '').slice(0, 1500)}`,
1488
1489
  ].join('\n');
@@ -1650,6 +1651,8 @@ export function countInfraFlakes(text = '') {
1650
1651
  /Navigation timeout of \d+\s*ms exceeded/g, // puppeteer page.goto
1651
1652
  /test timed out after \d+\s*ms/g, // node --test per-test timeout
1652
1653
  /\bTarget closed\b/g, // puppeteer: browser died mid-test
1654
+ /puppeteer-core is not installed next to the Manager/g, // worktree/node_modules missing
1655
+ /Cannot find module ['"]puppeteer-core['"]/g, // same dependency failure, alternate Node wording
1653
1656
  ];
1654
1657
  return pats.reduce((n, re) => n + (String(text ?? '').match(re) || []).length, 0);
1655
1658
  }
@@ -3120,6 +3123,12 @@ export function workerPrompt(task, goal, lastFailure, agentName = 'claude-code',
3120
3123
  // TASK/DETAIL below is a paraphrase from the planner — when they differ,
3121
3124
  // follow the user's words.
3122
3125
  goal?.text ? `USER'S REQUEST (verbatim — the source of truth; deliver exactly this):\n${goal.text}` : '',
3126
+ // A review reply is a new, explicit acceptance condition. It used to be
3127
+ // present only as TASK DETAIL below the inherited session context, which
3128
+ // made it easy for a resumed agent to keep describing the first attempt.
3129
+ // Put it next to the original request and make the precedence explicit:
3130
+ // the interface transports the user's words, it does not reinterpret them.
3131
+ task?.reply && task?.detail ? `CURRENT USER FOLLOW-UP (verbatim — these are the acceptance conditions for THIS re-review):\n${task.detail}` : '',
3123
3132
  runContext ? (typeof runContext === 'string' ? runContext : buildWorkerContextBlock(runContext)) : '',
3124
3133
  '',
3125
3134
  `TASK ${task.num}: ${task.title}`,
@@ -3144,6 +3153,7 @@ export function workerPrompt(task, goal, lastFailure, agentName = 'claude-code',
3144
3153
  // instead of expanding scope"): the request, not our fear of scope, sets
3145
3154
  // the size of the work.
3146
3155
  '- Deliver what the request actually asks for. If it asks for multiple options, N variations, a visual, an artifact, or a full exploration, produce ALL of them — do not reduce it to one, and do not cut it short to finish faster.',
3156
+ '- For a review follow-up, implement and report against CURRENT USER FOLLOW-UP above. Do not claim the first attempt as this attempt\'s result. Do not change shared theme tokens, seed/demo data, or unrelated indicators merely to make a manual check easier unless the user explicitly requested those changes; put test fixtures in test files only.',
3147
3157
  '- デザイン案・UI案・レイアウト案を求められた場合は、文章だけで完了にしない。画像を使わない案でも、比較して確認できるHTML等のartifactをプロジェクト内に作り、実際に開ける .manager-run.json を残す。',
3148
3158
  '- Keep the work bounded to the request: inspect only files needed; avoid broad repository scans unless they are necessary.',
3149
3159
  "- You may use the user's installed skills (Skill tool, e.g. /galda-doc) when one clearly fits the request.",
package/engine/server.mjs CHANGED
@@ -2991,11 +2991,26 @@ function testNodePath(dir, dependencySourceDir = null) {
2991
2991
  .filter(Boolean).filter(existsSync);
2992
2992
  return [...new Set([...local, ...(process.env.NODE_PATH ? process.env.NODE_PATH.split(sep) : [])])].join(sep);
2993
2993
  }
2994
+ function testEnvironmentPreflight(dir, dependencySourceDir = null) {
2995
+ const nodePath = testNodePath(dir, dependencySourceDir);
2996
+ const sources = nodePath ? nodePath.split(process.platform === 'win32' ? ';' : ':') : [];
2997
+ const hasPuppeteer = sources.some((p) => existsSync(join(p, 'puppeteer-core', 'package.json')));
2998
+ const npm = spawnSync('npm', ['--version'], { encoding: 'utf8' });
2999
+ const checks = [
3000
+ { id: 'node', ok: Boolean(process.execPath), detail: process.version },
3001
+ { id: 'npm', ok: npm.status === 0, detail: npm.status === 0 ? npm.stdout.trim() : 'npm command unavailable' },
3002
+ { id: 'dependencies', ok: sources.length > 0, detail: sources.length ? `${sources.length} node_modules source(s)` : 'node_modules not found' },
3003
+ { id: 'puppeteer', ok: hasPuppeteer, detail: hasPuppeteer ? 'puppeteer-core available' : 'puppeteer-core missing; run npm install in the project directory' },
3004
+ ];
3005
+ return { ok: checks.every((c) => c.ok), checks };
3006
+ }
2994
3007
  function runTestsOnce(dir, dependencySourceDir = null) {
2995
3008
  // The timeout is env-overridable so tests can force the killed-run path fast;
2996
3009
  // production keeps the 4-minute default.
2997
3010
  const runTimeout = Number(process.env.MANAGER_TEST_TIMEOUT_MS) || 240000;
2998
3011
  return testRunQueue.run(() => new Promise((res) => {
3012
+ const preflight = testEnvironmentPreflight(dir, dependencySourceDir);
3013
+ 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: [] });
2999
3014
  const nodePath = testNodePath(dir, dependencySourceDir);
3000
3015
  execFile('npm', ['test'], { cwd: dir, timeout: runTimeout, env: { ...process.env, ...(nodePath ? { NODE_PATH: nodePath } : {}) }, maxBuffer: 8 * 1024 * 1024 }, (e, out = '', err = '') => {
3001
3016
  const text = `${out}\n${err}`;
@@ -3025,7 +3040,7 @@ function runTestsOnce(dir, dependencySourceDir = null) {
3025
3040
  const timedOut = Boolean(e && (e.killed || e.signal === 'SIGTERM' || e.code === 'ETIMEDOUT'));
3026
3041
  // Capture the failing test NAMES too (not just the count) so the gate can
3027
3042
  // diff them against the base's failures and block only on NEW ones.
3028
- res({ passed, failed, timedOut, realAssertion, infraOnly, infraFlakes, detail: testOutputExcerpt(text), failingNames: parseFailingTestNames(text) });
3043
+ res({ passed, failed, preflight, timedOut, realAssertion, infraOnly, infraFlakes, detail: testOutputExcerpt(text), failingNames: parseFailingTestNames(text) });
3029
3044
  });
3030
3045
  }));
3031
3046
  }
@@ -3054,11 +3069,12 @@ async function runProjectTests(dir, dependencySourceDir = null) {
3054
3069
  // the gate should see, so no good goal is blocked on a saturated boot.
3055
3070
  infraOnly: !!kept.infraOnly,
3056
3071
  infraFlakes: kept.infraFlakes ?? 0,
3072
+ preflight: kept.preflight ?? r.preflight ?? null,
3057
3073
  detail: kept.detail || r.detail || r2.detail || '',
3058
3074
  failingNames: kept.failingNames ?? r.failingNames ?? [],
3059
3075
  };
3060
3076
  }
3061
- return { ran: true, passed: r.passed, failed: r.failed, ok: r.failed === 0, timedOut: !!r.timedOut, realAssertion: !!r.realAssertion, infraOnly: !!r.infraOnly, infraFlakes: r.infraFlakes ?? 0, detail: r.detail ?? '', failingNames: r.failingNames ?? [] };
3077
+ return { ran: true, passed: r.passed, failed: r.failed, ok: r.failed === 0, timedOut: !!r.timedOut, realAssertion: !!r.realAssertion, infraOnly: !!r.infraOnly, infraFlakes: r.infraFlakes ?? 0, preflight: r.preflight ?? null, detail: r.detail ?? '', failingNames: r.failingNames ?? [] };
3062
3078
  }
3063
3079
 
3064
3080
  // Failing tests on the BASE (pre-change) checkout — the set the review gate
@@ -3241,7 +3257,11 @@ async function summarizeForReview(goal, siblings, dir, reviewDefinition) {
3241
3257
  // An explicit ja/en on the project's review definition always overrides.
3242
3258
  const lang = reviewDefinition.language === 'auto' ? detectRequestLanguage(goal.text) : (reviewDefinition.language || 'ja');
3243
3259
  const changed = [...new Set(siblings.flatMap((t) => t.changedFiles ?? []))].slice(0, 20);
3244
- const reports = siblings.filter((t) => !t.reply).map((t) => `- ${t.title}: ${(t.result ?? '').replace(/\s+/g, ' ').slice(0, 300)}`).join('\n');
3260
+ // A reply is a new implementation attempt, not just conversation. Excluding
3261
+ // it here made the second Review render the first attempt's stale Result,
3262
+ // even when the worker had completed the user's latest feedback.
3263
+ const reports = siblings.map((t) => `${t.reply ? '[follow-up] ' : ''}- ${t.title}: ${(t.result ?? '').replace(/\s+/g, ' ').slice(0, 300)}`).join('\n');
3264
+ const latestFeedback = [...siblings].reverse().find((t) => t.reply && t.detail)?.detail ?? '';
3245
3265
  // The name of the work, from the agent that did it (task.run.title). It wins
3246
3266
  // over both the summarizer and the request text: the summarizer only ever read
3247
3267
  // the same request the user typed, and the request text is the sentence the
@@ -3255,7 +3275,7 @@ async function summarizeForReview(goal, siblings, dir, reviewDefinition) {
3255
3275
  changed: (goal.plan?.[0] ?? goal.text ?? '').replace(/\s+/g, ' ').slice(0, 80),
3256
3276
  };
3257
3277
  if (!changed.length && !reports) return fallback;
3258
- const prompt = buildReviewSummaryPrompt({ lang, goalText: goal.text, changed, reports });
3278
+ const prompt = buildReviewSummaryPrompt({ lang, goalText: goal.text, changed, reports, latestFeedback });
3259
3279
  try {
3260
3280
  const r = await runUtility({ prompt, cwd: dir, tools: 'Read' });
3261
3281
  const summary = parseReviewSummary(r.result, fallback);
@@ -3519,7 +3539,7 @@ async function verifyGate(goal, prProject, siblings, baseProject, activityTask =
3519
3539
  const testGateOn = resolveTestGate({ reviewDefinition, env: process.env, force: forceTests });
3520
3540
  if (testGateOn && hasTestRelevantChanges(changedFiles)) {
3521
3541
  goalAct(`Running project tests in the goal worktree (${changedFiles.length} changed file${changedFiles.length === 1 ? '' : 's'}).`);
3522
- goal.testResult = await runProjectTests(wdir, project.dir);
3542
+ goal.testResult = await runProjectTests(wdir, baseProject?.dir);
3523
3543
  goalAct(`Project tests finished: ${goal.testResult.failed ?? 0} failed, ${goal.testResult.passed ?? 0} passed.`);
3524
3544
  } else if (!testGateOn) {
3525
3545
  goal.testResult = { ran: false, skipped: true, skippedReason: 'test gate off (opt-in: requireTests / MANAGER_VERIFY_GATE=on)' };
@@ -3571,6 +3591,11 @@ async function verifyGate(goal, prProject, siblings, baseProject, activityTask =
3571
3591
  if (goal.testResult.ran && goal.testResult.infraOnly && !goal.testResult.inconclusive) {
3572
3592
  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, ' '));
3573
3593
  }
3594
+ if (goal.testResult.ran && goal.testResult.preflight && !goal.testResult.preflight.ok) {
3595
+ const missing = goal.testResult.preflight.checks.filter((c) => !c.ok).map((c) => c.id).join(', ');
3596
+ goal.testResult.inconclusive = true;
3597
+ 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.`);
3598
+ }
3574
3599
  // Baseline diff (Masa 2026-07-19): block ONLY on failures THIS change
3575
3600
  // introduced. A test already red on the base — e.g. persist-failure on clean
3576
3601
  // origin/main — is pre-existing noise and must not knock a proof-verified
@@ -3942,6 +3967,23 @@ const server = createServer(async (req, res) => {
3942
3967
  return json(res, 200, { projects: projectsWithBranch, goals: visibleGoals, tasks: trimTaskActivityForState(visibleTasks), models: MODELS, agentModels: { 'claude-code': MODELS, codex: CODEX_MODELS }, agentEfforts: { 'claude-code': CLAUDE_EFFORTS, codex: CODEX_EFFORTS }, agents: AVAILABLE_AGENTS, connectCommand, queueOrder, externalActivity, workflowColumns: projectColumns, reviewDefinitions: projectReviewDefs, runningCounts, parallelLimits, uiVersion: uiVersion(), billing, auth: publicAuth() });
3943
3968
  }
3944
3969
 
3970
+ // Read-only stability snapshot: separates implementation failures from
3971
+ // verification infrastructure so operational trends can be monitored without
3972
+ // scraping human-facing cards. The browser dashboard can consume this later.
3973
+ if (url.pathname === '/api/stability' && req.method === 'GET') {
3974
+ const since = Date.now() - 24 * 60 * 60 * 1000;
3975
+ const recentGoals = goals.filter((g) => Date.parse(g.createdAt ?? '') >= since);
3976
+ const count = (fn) => recentGoals.filter(fn).length;
3977
+ return json(res, 200, {
3978
+ since: new Date(since).toISOString(), generatedAt: new Date().toISOString(), total: recentGoals.length,
3979
+ implementationFailures: count((g) => ['failed', 'partial'].includes(g.status) && !g.verificationInfraError),
3980
+ verificationInfrastructure: count((g) => Boolean(g.verificationInfraError)),
3981
+ timeoutsOrInconclusive: count((g) => Boolean(g.testResult?.inconclusive)),
3982
+ workerHolds: count((g) => ['rate-limit', 'budget'].includes(g.blocked?.kind)),
3983
+ readyForReview: count((g) => g.status === 'review'),
3984
+ });
3985
+ }
3986
+
3945
3987
  // Billing (docs/BILLING-LAUNCH-PLAN.md): activate the license key a user
3946
3988
  // copies from galda.app after paying. GET reports whether one is on file
3947
3989
  // (for the settings panel); POST verifies+persists a newly pasted token.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@galda/cli",
3
- "version": "0.10.83",
3
+ "version": "0.10.84",
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": {