@galda/cli 0.10.82 → 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 +86 -12
- 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
|
@@ -2982,12 +2982,37 @@ const testRunQueue = createSerialQueue();
|
|
|
2982
2982
|
// Manager-run tests (never trust the worker's "all green" claim): run the
|
|
2983
2983
|
// project's own `npm test` in the goal's worktree and parse node --test's
|
|
2984
2984
|
// "ℹ pass N / ℹ fail N" (or TAP "# pass/# fail"). Returns {ran,passed,failed,ok}.
|
|
2985
|
-
function
|
|
2985
|
+
function testNodePath(dir, dependencySourceDir = null) {
|
|
2986
|
+
// node_modules is intentionally absent from git worktrees. Reuse the base
|
|
2987
|
+
// checkout's installed dependencies before declaring every server/UI test a
|
|
2988
|
+
// product regression; verification must never silently depend on `npm install`.
|
|
2989
|
+
const sep = process.platform === 'win32' ? ';' : ':';
|
|
2990
|
+
const local = [join(dir, 'node_modules'), dependencySourceDir && join(dependencySourceDir, 'node_modules'), join(ROOT, 'node_modules')]
|
|
2991
|
+
.filter(Boolean).filter(existsSync);
|
|
2992
|
+
return [...new Set([...local, ...(process.env.NODE_PATH ? process.env.NODE_PATH.split(sep) : [])])].join(sep);
|
|
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
|
+
}
|
|
3007
|
+
function runTestsOnce(dir, dependencySourceDir = null) {
|
|
2986
3008
|
// The timeout is env-overridable so tests can force the killed-run path fast;
|
|
2987
3009
|
// production keeps the 4-minute default.
|
|
2988
3010
|
const runTimeout = Number(process.env.MANAGER_TEST_TIMEOUT_MS) || 240000;
|
|
2989
3011
|
return testRunQueue.run(() => new Promise((res) => {
|
|
2990
|
-
|
|
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: [] });
|
|
3014
|
+
const nodePath = testNodePath(dir, dependencySourceDir);
|
|
3015
|
+
execFile('npm', ['test'], { cwd: dir, timeout: runTimeout, env: { ...process.env, ...(nodePath ? { NODE_PATH: nodePath } : {}) }, maxBuffer: 8 * 1024 * 1024 }, (e, out = '', err = '') => {
|
|
2991
3016
|
const text = `${out}\n${err}`;
|
|
2992
3017
|
const n = (re) => { const m = text.match(re); return m ? Number(m[1]) : null; };
|
|
2993
3018
|
const passed = n(/ℹ pass (\d+)/) ?? n(/# pass (\d+)/) ?? n(/(\d+) passing/) ?? 0;
|
|
@@ -3015,21 +3040,21 @@ function runTestsOnce(dir) {
|
|
|
3015
3040
|
const timedOut = Boolean(e && (e.killed || e.signal === 'SIGTERM' || e.code === 'ETIMEDOUT'));
|
|
3016
3041
|
// Capture the failing test NAMES too (not just the count) so the gate can
|
|
3017
3042
|
// diff them against the base's failures and block only on NEW ones.
|
|
3018
|
-
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) });
|
|
3019
3044
|
});
|
|
3020
3045
|
}));
|
|
3021
3046
|
}
|
|
3022
|
-
async function runProjectTests(dir) {
|
|
3047
|
+
async function runProjectTests(dir, dependencySourceDir = null) {
|
|
3023
3048
|
let hasTest = false;
|
|
3024
3049
|
try { hasTest = !!JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')).scripts?.test; } catch { /* no package.json */ }
|
|
3025
3050
|
if (!hasTest) return { ran: false };
|
|
3026
|
-
let r = await runTestsOnce(dir);
|
|
3051
|
+
let r = await runTestsOnce(dir, dependencySourceDir);
|
|
3027
3052
|
// Flake resistance: the integration suite spawns real servers; under load a
|
|
3028
3053
|
// startup can time out. Re-run once and take the fewer failures — a real
|
|
3029
3054
|
// failure fails both runs, a flake clears on the retry — so a good goal is
|
|
3030
3055
|
// never blocked on a flaky test.
|
|
3031
3056
|
if (r.failed > 0) {
|
|
3032
|
-
const r2 = await runTestsOnce(dir);
|
|
3057
|
+
const r2 = await runTestsOnce(dir, dependencySourceDir);
|
|
3033
3058
|
const kept = r2.failed <= r.failed ? r2 : r; // the run that saw fewer failures
|
|
3034
3059
|
r = {
|
|
3035
3060
|
passed: Math.max(r.passed, r2.passed),
|
|
@@ -3044,11 +3069,12 @@ async function runProjectTests(dir) {
|
|
|
3044
3069
|
// the gate should see, so no good goal is blocked on a saturated boot.
|
|
3045
3070
|
infraOnly: !!kept.infraOnly,
|
|
3046
3071
|
infraFlakes: kept.infraFlakes ?? 0,
|
|
3072
|
+
preflight: kept.preflight ?? r.preflight ?? null,
|
|
3047
3073
|
detail: kept.detail || r.detail || r2.detail || '',
|
|
3048
3074
|
failingNames: kept.failingNames ?? r.failingNames ?? [],
|
|
3049
3075
|
};
|
|
3050
3076
|
}
|
|
3051
|
-
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 ?? [] };
|
|
3052
3078
|
}
|
|
3053
3079
|
|
|
3054
3080
|
// Failing tests on the BASE (pre-change) checkout — the set the review gate
|
|
@@ -3144,7 +3170,7 @@ async function runRetestJob(goal, token) {
|
|
|
3144
3170
|
} else {
|
|
3145
3171
|
for (let i = 0; i < RETEST_RUNS; i++) {
|
|
3146
3172
|
if (!live()) return; // cancelled (UNDO) / superseded / deleted → discard
|
|
3147
|
-
const r = await runTestsOnce(wdir);
|
|
3173
|
+
const r = await runTestsOnce(wdir, project.dir);
|
|
3148
3174
|
if (!live()) return; // cancelled while this run was in flight
|
|
3149
3175
|
results.push({ ran: true, passed: r.passed, failed: r.failed });
|
|
3150
3176
|
goal.retest = { state: 'running', runs: results.length, total: RETEST_RUNS, startedAt: goal.retest?.startedAt };
|
|
@@ -3231,7 +3257,11 @@ async function summarizeForReview(goal, siblings, dir, reviewDefinition) {
|
|
|
3231
3257
|
// An explicit ja/en on the project's review definition always overrides.
|
|
3232
3258
|
const lang = reviewDefinition.language === 'auto' ? detectRequestLanguage(goal.text) : (reviewDefinition.language || 'ja');
|
|
3233
3259
|
const changed = [...new Set(siblings.flatMap((t) => t.changedFiles ?? []))].slice(0, 20);
|
|
3234
|
-
|
|
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 ?? '';
|
|
3235
3265
|
// The name of the work, from the agent that did it (task.run.title). It wins
|
|
3236
3266
|
// over both the summarizer and the request text: the summarizer only ever read
|
|
3237
3267
|
// the same request the user typed, and the request text is the sentence the
|
|
@@ -3245,7 +3275,7 @@ async function summarizeForReview(goal, siblings, dir, reviewDefinition) {
|
|
|
3245
3275
|
changed: (goal.plan?.[0] ?? goal.text ?? '').replace(/\s+/g, ' ').slice(0, 80),
|
|
3246
3276
|
};
|
|
3247
3277
|
if (!changed.length && !reports) return fallback;
|
|
3248
|
-
const prompt = buildReviewSummaryPrompt({ lang, goalText: goal.text, changed, reports });
|
|
3278
|
+
const prompt = buildReviewSummaryPrompt({ lang, goalText: goal.text, changed, reports, latestFeedback });
|
|
3249
3279
|
try {
|
|
3250
3280
|
const r = await runUtility({ prompt, cwd: dir, tools: 'Read' });
|
|
3251
3281
|
const summary = parseReviewSummary(r.result, fallback);
|
|
@@ -3509,7 +3539,7 @@ async function verifyGate(goal, prProject, siblings, baseProject, activityTask =
|
|
|
3509
3539
|
const testGateOn = resolveTestGate({ reviewDefinition, env: process.env, force: forceTests });
|
|
3510
3540
|
if (testGateOn && hasTestRelevantChanges(changedFiles)) {
|
|
3511
3541
|
goalAct(`Running project tests in the goal worktree (${changedFiles.length} changed file${changedFiles.length === 1 ? '' : 's'}).`);
|
|
3512
|
-
goal.testResult = await runProjectTests(wdir);
|
|
3542
|
+
goal.testResult = await runProjectTests(wdir, baseProject?.dir);
|
|
3513
3543
|
goalAct(`Project tests finished: ${goal.testResult.failed ?? 0} failed, ${goal.testResult.passed ?? 0} passed.`);
|
|
3514
3544
|
} else if (!testGateOn) {
|
|
3515
3545
|
goal.testResult = { ran: false, skipped: true, skippedReason: 'test gate off (opt-in: requireTests / MANAGER_VERIFY_GATE=on)' };
|
|
@@ -3561,6 +3591,11 @@ async function verifyGate(goal, prProject, siblings, baseProject, activityTask =
|
|
|
3561
3591
|
if (goal.testResult.ran && goal.testResult.infraOnly && !goal.testResult.inconclusive) {
|
|
3562
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, ' '));
|
|
3563
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
|
+
}
|
|
3564
3599
|
// Baseline diff (Masa 2026-07-19): block ONLY on failures THIS change
|
|
3565
3600
|
// introduced. A test already red on the base — e.g. persist-failure on clean
|
|
3566
3601
|
// origin/main — is pre-existing noise and must not knock a proof-verified
|
|
@@ -3932,6 +3967,23 @@ const server = createServer(async (req, res) => {
|
|
|
3932
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() });
|
|
3933
3968
|
}
|
|
3934
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
|
+
|
|
3935
3987
|
// Billing (docs/BILLING-LAUNCH-PLAN.md): activate the license key a user
|
|
3936
3988
|
// copies from galda.app after paying. GET reports whether one is on file
|
|
3937
3989
|
// (for the settings panel); POST verifies+persists a newly pasted token.
|
|
@@ -4185,6 +4237,16 @@ const server = createServer(async (req, res) => {
|
|
|
4185
4237
|
for (const t of removedTasks) logAppend(JSON.stringify({ kind: 'task', id: t.id, deleted: true }));
|
|
4186
4238
|
projects.splice(idx, 1);
|
|
4187
4239
|
queues.delete(project.id);
|
|
4240
|
+
// A deleted project's goals must not outlive it: project ids are derived
|
|
4241
|
+
// from the name (uniqueProjectId), so a later "+ New project" click can
|
|
4242
|
+
// regenerate the exact same id and — without this purge — inherit the
|
|
4243
|
+
// old project's leftover goals as if they belonged to the brand-new one
|
|
4244
|
+
// (Masa report 2026-07-27: new project shows Review items on creation).
|
|
4245
|
+
for (const g of goals.filter((g) => g.projectId === project.id)) {
|
|
4246
|
+
goals = goals.filter((x) => x.id !== g.id);
|
|
4247
|
+
logAppend(JSON.stringify({ kind: 'goal', id: g.id, deleted: true }));
|
|
4248
|
+
sendGoalEvent({ ev: 'goal-deleted', id: g.id }, g);
|
|
4249
|
+
}
|
|
4188
4250
|
saveProjects();
|
|
4189
4251
|
send({ ev: 'projects', projects, deletedProjectId: project.id });
|
|
4190
4252
|
return json(res, 200, { ok: true, projects, deletedProjectId: project.id });
|
|
@@ -5418,7 +5480,19 @@ async function answerGoalQuestion(goal, question) {
|
|
|
5418
5480
|
if (upMatch) {
|
|
5419
5481
|
const file = join(logDir, 'uploads', upMatch[1]);
|
|
5420
5482
|
if (!existsSync(file)) { res.writeHead(404); return res.end('not found'); }
|
|
5421
|
-
|
|
5483
|
+
// Worker-declared Open it artifacts and image attachments share `/upload`.
|
|
5484
|
+
// Never guess that an unknown artifact is a PNG: it turns valid HTML, PDF,
|
|
5485
|
+
// or text into a broken-image page. Known preview formats get their real
|
|
5486
|
+
// browser type; an unknown file downloads safely instead of being misread.
|
|
5487
|
+
const ext = (file.match(/\.([a-z0-9]+)$/i)?.[1] || '').toLowerCase();
|
|
5488
|
+
const types = {
|
|
5489
|
+
html: 'text/html; charset=utf-8', htm: 'text/html; charset=utf-8',
|
|
5490
|
+
css: 'text/css; charset=utf-8', js: 'text/javascript; charset=utf-8', mjs: 'text/javascript; charset=utf-8',
|
|
5491
|
+
json: 'application/json; charset=utf-8', txt: 'text/plain; charset=utf-8', md: 'text/markdown; charset=utf-8', csv: 'text/csv; charset=utf-8',
|
|
5492
|
+
svg: 'image/svg+xml', png: 'image/png', gif: 'image/gif', jpg: 'image/jpeg', jpeg: 'image/jpeg', webp: 'image/webp', ico: 'image/x-icon',
|
|
5493
|
+
pdf: 'application/pdf', mp4: 'video/mp4', webm: 'video/webm', wasm: 'application/wasm',
|
|
5494
|
+
};
|
|
5495
|
+
res.writeHead(200, { 'content-type': types[ext] || 'application/octet-stream' });
|
|
5422
5496
|
return res.end(readFileSync(file));
|
|
5423
5497
|
}
|
|
5424
5498
|
|
package/package.json
CHANGED