@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 +16 -2
- package/engine/lib.mjs +11 -1
- package/engine/server.mjs +47 -5
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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,
|
|
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