@galda/cli 0.10.57 → 0.10.60
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 +19 -3
- package/engine/lib.mjs +20 -0
- package/engine/server.mjs +33 -7
- package/package.json +1 -1
package/app/index.html
CHANGED
|
@@ -7055,9 +7055,10 @@ function renderFsTodo(){
|
|
|
7055
7055
|
for (const b of el.querySelectorAll('[data-fslog]')) b.onclick = () => fsToggleLog(Number(b.dataset.fslog));
|
|
7056
7056
|
for (const b of el.querySelectorAll('[data-fsglog]')) b.onclick = () => toggleGoalDetail(Number(b.dataset.fsglog));
|
|
7057
7057
|
for (const b of el.querySelectorAll('[data-fsreverify]')) b.onclick = async () => {
|
|
7058
|
-
b.disabled = true; b.textContent = '
|
|
7058
|
+
b.disabled = true; b.textContent = 'Starting…';
|
|
7059
7059
|
const r = await fetch(withKey(`/api/goals/${b.dataset.fsreverify}/reverify`), { method: 'POST' });
|
|
7060
|
-
if (!r.ok) showErr('
|
|
7060
|
+
if (!r.ok) { showErr(document.documentElement.lang === 'ja' ? 'Retryに失敗しました' : 'Retry failed.'); b.disabled = false; b.textContent = 'Retry'; }
|
|
7061
|
+
else await refresh();
|
|
7061
7062
|
};
|
|
7062
7063
|
for (const b of el.querySelectorAll('[data-fsarchive]')) b.onclick = async () => {
|
|
7063
7064
|
const r = await fetch(withKey(`/api/goals/${b.dataset.fsarchive}/archive`), { method: 'POST' });
|
|
@@ -9452,6 +9453,11 @@ async function saEnhanceShotBoxes(){
|
|
|
9452
9453
|
}
|
|
9453
9454
|
function saRender(){
|
|
9454
9455
|
const S = $('seeall'); if (!S) return;
|
|
9456
|
+
// SSE, diff fetches and action responses rebuild the dialog. Remember the
|
|
9457
|
+
// review body's position before replacing it so reading never jumps to top.
|
|
9458
|
+
const previousBody = S.querySelector('.sa-body');
|
|
9459
|
+
const previousScrollTop = previousBody?.scrollTop ?? 0;
|
|
9460
|
+
const restoreScroll = Boolean(previousBody) && SA.focusGid == null;
|
|
9455
9461
|
const pending = SA.items.filter((x) => x.st === 0);
|
|
9456
9462
|
const green = pending.filter((x) => !x.testResult || x.testResult.ok !== false).length;
|
|
9457
9463
|
// Solo mode = a single "Needs you" goal opened as this card. It is not a review batch, so the
|
|
@@ -9464,6 +9470,16 @@ function saRender(){
|
|
|
9464
9470
|
const hdKeys = solo ? '↑↓ MOVE · ESC CLOSE' : '↑↓ MOVE · A APPROVE · D DISMISS · ESC CLOSE';
|
|
9465
9471
|
S.innerHTML = `<div class="sa-panel"><div class="sa-hd"><span class="t">${hdTitle}</span><span class="c">${c}</span><span class="sa-count">${posLabel}</span><span class="keys">${hdKeys}</span><button class="sa-x" data-saact="close:0">✕</button></div>
|
|
9466
9472
|
<div class="sa-body">${SA.items.length ? SA.items.map((it, i) => saItemHtml(it, i)).join('') : '<div class="sa-empty">Nothing waiting for review.</div>'}</div></div>`;
|
|
9473
|
+
const body = S.querySelector('.sa-body');
|
|
9474
|
+
if (body && restoreScroll) body.scrollTop = previousScrollTop;
|
|
9475
|
+
// Once the person scrolls, their position wins over the short opening-focus
|
|
9476
|
+
// animation. Programmatic opening scroll fires this too, which is fine: the
|
|
9477
|
+
// first placement happened and later SSE ticks should preserve it.
|
|
9478
|
+
body?.addEventListener('scroll', () => {
|
|
9479
|
+
if (SA.focusGid == null) return;
|
|
9480
|
+
SA.focusGid = null;
|
|
9481
|
+
if (SA.focusTimer) { clearTimeout(SA.focusTimer); SA.focusTimer = null; }
|
|
9482
|
+
}, { once: true });
|
|
9467
9483
|
// Native <details> would collapse on every rebuild (SSE tick, approve, etc.) —
|
|
9468
9484
|
// track what's open per goal+section in SA.detOpen (persists across re-renders)
|
|
9469
9485
|
// and wire each one's toggle to keep that set current.
|
|
@@ -9779,7 +9795,7 @@ $('seeall').addEventListener('click', async (e) => {
|
|
|
9779
9795
|
if (link) link.disabled = true;
|
|
9780
9796
|
if (v) v.textContent = 'Starting it…';
|
|
9781
9797
|
let r = null, body = {};
|
|
9782
|
-
try { r = await fetch(`/api/tasks/${it.run.taskId}/preview
|
|
9798
|
+
try { r = await fetch(withKey(`/api/tasks/${it.run.taskId}/preview`), { method: 'POST' }); body = await r.json().catch(() => ({})); }
|
|
9783
9799
|
catch (e) { body = { error: String(e.message ?? e) }; }
|
|
9784
9800
|
if (link) link.disabled = false;
|
|
9785
9801
|
const noteText = it.run.note || it.run.cmd || 'Starts when you press it.';
|
package/engine/lib.mjs
CHANGED
|
@@ -1646,6 +1646,25 @@ export function detectRequestLanguage(text) {
|
|
|
1646
1646
|
return /[-ヿ㐀-鿿]/.test(text || '') ? 'ja' : 'en';
|
|
1647
1647
|
}
|
|
1648
1648
|
|
|
1649
|
+
export function testFailureReason({ text = '', count = 0, attempts = null } = {}) {
|
|
1650
|
+
const n = Number(count) || 0;
|
|
1651
|
+
const tries = attempts == null ? null : Number(attempts) || 0;
|
|
1652
|
+
if (detectRequestLanguage(text) === 'ja') {
|
|
1653
|
+
return tries == null ? `テスト失敗 ${n} 件` : `テスト失敗 ${n} 件(自動修正 ${tries} 回後も失敗)`;
|
|
1654
|
+
}
|
|
1655
|
+
const tests = `${n} test${n === 1 ? '' : 's'} failed`;
|
|
1656
|
+
return tries == null ? tests : `${tests} after ${tries} automatic fix attempt${tries === 1 ? '' : 's'}`;
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
export function manualTestRetryPrompt({ text = '', count = 0, detail = '' } = {}) {
|
|
1660
|
+
const n = Number(count) || 0;
|
|
1661
|
+
const output = String(detail || '(no detailed output captured)').slice(0, 3200);
|
|
1662
|
+
if (detectRequestLanguage(text) === 'ja') {
|
|
1663
|
+
return `Retryが押されました。前回のテスト失敗 ${n} 件を確認し、元のタスクの作業環境で原因を修正してからテストを再実行してください。関係のないリファクタは行わないでください。\n\n前回のテスト出力:\n${output}`;
|
|
1664
|
+
}
|
|
1665
|
+
return `Retry was requested. Inspect the previous ${n} failing test${n === 1 ? '' : 's'}, fix the cause in the original task worktree, then rerun the tests. Do not make unrelated changes.\n\nPrevious test output:\n${output}`;
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1649
1668
|
// SLICE 2 intent routing (docs/HANDOFF-ONBOARDING-conversational.md): the single
|
|
1650
1669
|
// composer is conversation-first — a chat/help/settings message ("what can you
|
|
1651
1670
|
// do?", "reply in English", a greeting) must get a conversational reply, NOT a
|
|
@@ -2583,6 +2602,7 @@ export function workerPrompt(task, goal, lastFailure, agentName = 'claude-code')
|
|
|
2583
2602
|
// (research, summaries, answers) simply skip this — the worker decides,
|
|
2584
2603
|
// never us.
|
|
2585
2604
|
`- 人が実際に動かして結果を見られる依頼なら、最後にプロジェクト直下へ ${RUN_DECL_FILE} を書く: {"cmd": "<起動コマンド>", "url": "<開くURL>", "note": "<何が見えるか1行>", "check": "<人に何を見て判断してほしいか1行>", "title": "<この作業のチケット名。対象が分かる程度に説明的で動作を含む短い名前。例「看板モードの追加」「ヘッダのズレの修正」。依頼文をそのまま写さない>"}。Manager はこれを使って人に「動かして見る」と「何を見ればいいか」を出す。動かして見せるものが無い依頼(調査・要約・質問への回答など)では書かなくてよい。`,
|
|
2605
|
+
`- 「ローカルで見れない」というフィードバックには、ターミナル操作や npx の再実行を案内して終わらないこと。${RUN_DECL_FILE} を実際に開ける内容へ更新し、可能なら自分でURLへ接続確認してから完了すること。`,
|
|
2586
2606
|
// Links are rendered as links. The product cannot tell a real one from a
|
|
2587
2607
|
// decorative one, so the only place this can be held is here.
|
|
2588
2608
|
`- 自分が作っていない場所へのリンクを報告に書かない(存在しないURLや「詳しくはここ」のような飾りのリンクは、そのままリンクとして人に表示される)。`,
|
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 } from './lib.mjs';
|
|
24
|
-
import { parseStreamEvents, parseCodexEvents, buildCodexArgs, parsePlan, refinePlanTasks, canEditGoal, canDeleteGoal, shouldAskClarification, workerExitReason, isForcedStop, taskStatusAfterVerify, workerResultText, taskCountsAsComplete, resolveWantsPR, resolveGoalSource, buildGoalSummary, 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, 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, hasTestRelevantChanges, parseFailingTestNames, classifyTestGate, isInconclusiveTestRun, countInfraFlakes, classifyRunFailures, classifyVerifyGate, 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, isNothingVerifiable, classifyComposerIntentHeuristic, detectPauseIntent, buildIntentPrompt, parseIntentResponse, buildBoardSnapshot, validateBoardSnapshot, boardIsEmpty, decideBoardPull, resolveConnectedAgents, pickAvailableAgent, pickUtilityAgent, utilityModel, liveTakeoverDecision, shouldOpenBrowserOnBoot, goalTaskTitle, buildGoalTask, RUN_DECL_FILE, parseRunDeclaration, SHOT_DIR, collectShotNames, parseGoalAddress, REPLY_OUTCOMES, REPLY_INTENTS, buildReplyIntentPrompt, parseReplyIntent, buildGoalMessage, serializeGoalMessage, parseGoalMessages } from './lib.mjs';
|
|
24
|
+
import { parseStreamEvents, parseCodexEvents, buildCodexArgs, parsePlan, refinePlanTasks, canEditGoal, canDeleteGoal, shouldAskClarification, workerExitReason, isForcedStop, taskStatusAfterVerify, workerResultText, taskCountsAsComplete, resolveWantsPR, resolveGoalSource, buildGoalSummary, 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, 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, hasTestRelevantChanges, parseFailingTestNames, classifyTestGate, isInconclusiveTestRun, countInfraFlakes, classifyRunFailures, classifyVerifyGate, 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, classifyComposerIntentHeuristic, detectPauseIntent, buildIntentPrompt, parseIntentResponse, buildBoardSnapshot, validateBoardSnapshot, boardIsEmpty, decideBoardPull, resolveConnectedAgents, pickAvailableAgent, pickUtilityAgent, utilityModel, liveTakeoverDecision, shouldOpenBrowserOnBoot, goalTaskTitle, buildGoalTask, RUN_DECL_FILE, parseRunDeclaration, SHOT_DIR, collectShotNames, parseGoalAddress, REPLY_OUTCOMES, REPLY_INTENTS, buildReplyIntentPrompt, parseReplyIntent, buildGoalMessage, serializeGoalMessage, parseGoalMessages } from './lib.mjs';
|
|
25
25
|
import { createSerialQueue } from './lib.mjs';
|
|
26
26
|
import { collectProjectRules } from './lib.mjs';
|
|
27
27
|
import { FALLBACK_CODEX_MODELS, loadCodexModels } from './codex-models.mjs';
|
|
@@ -2129,7 +2129,7 @@ async function startEphemeralApp(projectDir) {
|
|
|
2129
2129
|
try {
|
|
2130
2130
|
const key = readFileSync(join(home, 'secret.key'), 'utf8').trim();
|
|
2131
2131
|
const r = await fetch(`http://localhost:${port}/api/state?key=${key}`);
|
|
2132
|
-
if (r.ok) return { url: `http://localhost:${port}/?key=${key}`, kill: () => { try { child.kill(); } catch {} } };
|
|
2132
|
+
if (r.ok) return { url: `http://localhost:${port}/?key=${key}`, child, kill: () => { try { child.kill(); } catch {} } };
|
|
2133
2133
|
} catch { /* not up yet */ }
|
|
2134
2134
|
}
|
|
2135
2135
|
try { child.kill(); } catch {}
|
|
@@ -2259,6 +2259,19 @@ async function startPreview(task) {
|
|
|
2259
2259
|
live.expires = Date.now() + PREVIEW_IDLE_MS;
|
|
2260
2260
|
return { url: live.url, reused: true };
|
|
2261
2261
|
}
|
|
2262
|
+
// A Manager app cannot preview itself on its declared production port: that
|
|
2263
|
+
// port is already occupied, and its bare URL requires an access key. Boot the
|
|
2264
|
+
// edited checkout with isolated state and return its real key-bearing URL.
|
|
2265
|
+
let managerPackage = false;
|
|
2266
|
+
try { managerPackage = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')).name === '@galda/cli'; } catch {}
|
|
2267
|
+
if (managerPackage && existsSync(join(dir, 'engine', 'server.mjs')) && existsSync(join(dir, 'app', 'index.html'))) {
|
|
2268
|
+
const eph = await startEphemeralApp(dir);
|
|
2269
|
+
if (!eph) throw new Error('Could not start the local preview. Please try again.');
|
|
2270
|
+
const entry = { proc: eph.child, url: eph.url, expires: Date.now() + PREVIEW_IDLE_MS };
|
|
2271
|
+
previews.set(task.id, entry);
|
|
2272
|
+
eph.child.on('close', () => { if (previews.get(task.id) === entry) previews.delete(task.id); });
|
|
2273
|
+
return { url: eph.url, reused: false, started: true };
|
|
2274
|
+
}
|
|
2262
2275
|
// Declared a URL but no command: the thing is already reachable (an app the
|
|
2263
2276
|
// worker left running, a hosted page). Nothing to start.
|
|
2264
2277
|
if (!cmd) return { url, reused: false, started: false };
|
|
@@ -3174,7 +3187,7 @@ async function verifyGate(goal, prProject, siblings, baseProject, activityTask =
|
|
|
3174
3187
|
if (testsFailing) {
|
|
3175
3188
|
const failure = recordGoalFailure(goal, {
|
|
3176
3189
|
kind: 'test',
|
|
3177
|
-
reason:
|
|
3190
|
+
reason: testFailureReason({ text: goal.text, count: blockingFailCount }),
|
|
3178
3191
|
changedFiles,
|
|
3179
3192
|
testResult: goal.testResult,
|
|
3180
3193
|
});
|
|
@@ -3186,7 +3199,7 @@ async function verifyGate(goal, prProject, siblings, baseProject, activityTask =
|
|
|
3186
3199
|
goal.autoRework = {
|
|
3187
3200
|
...(goal.autoRework ?? {}),
|
|
3188
3201
|
testFailures: used + 1,
|
|
3189
|
-
lastReason:
|
|
3202
|
+
lastReason: testFailureReason({ text: goal.text, count: blockingFailCount }),
|
|
3190
3203
|
lastAt: new Date().toISOString(),
|
|
3191
3204
|
};
|
|
3192
3205
|
goal.blocked = null;
|
|
@@ -3232,7 +3245,7 @@ async function verifyGate(goal, prProject, siblings, baseProject, activityTask =
|
|
|
3232
3245
|
}
|
|
3233
3246
|
goal.blocked = {
|
|
3234
3247
|
kind: gate.kind,
|
|
3235
|
-
reason: testsFailing ?
|
|
3248
|
+
reason: testsFailing ? testFailureReason({ text: goal.text, count: blockingFailCount, attempts: goal.autoRework?.testFailures ?? 0 })
|
|
3236
3249
|
: nothingVerifiable ? '検証条件つきのタスクなのに、ファイル変更も proof も無い(約束した検証が行われていない)'
|
|
3237
3250
|
: '検証(proof)が全タスク分そろっていない(requireVerifyPass)',
|
|
3238
3251
|
};
|
|
@@ -4525,8 +4538,8 @@ const server = createServer(async (req, res) => {
|
|
|
4525
4538
|
return json(res, 200, goal);
|
|
4526
4539
|
}
|
|
4527
4540
|
|
|
4528
|
-
//
|
|
4529
|
-
//
|
|
4541
|
+
// Retry a test-blocked goal by sending the saved failure back to its worker.
|
|
4542
|
+
// Other blocked states still re-run the verification gate in place.
|
|
4530
4543
|
const reverifyMatch = url.pathname.match(/^\/api\/goals\/(\d+)\/reverify$/);
|
|
4531
4544
|
if (reverifyMatch && req.method === 'POST') {
|
|
4532
4545
|
const goal = goals.find((g) => g.id === Number(reverifyMatch[1]));
|
|
@@ -4535,6 +4548,19 @@ const server = createServer(async (req, res) => {
|
|
|
4535
4548
|
const project = projects.find((p) => p.id === goal.projectId);
|
|
4536
4549
|
if (!project) return json(res, 404, { error: 'project not found' });
|
|
4537
4550
|
const siblings = tasks.filter((t) => t.goalId === goal.id);
|
|
4551
|
+
if (goal.status === 'blocked' && goal.blocked?.kind === 'test') {
|
|
4552
|
+
const count = goal.testResult?.newFailures?.length ?? goal.testResult?.failed ?? 0;
|
|
4553
|
+
const prompt = manualTestRetryPrompt({ text: goal.text, count, detail: goal.testResult?.detail });
|
|
4554
|
+
goal.status = 'running';
|
|
4555
|
+
goal.blocked = null;
|
|
4556
|
+
goal.startedAt = null;
|
|
4557
|
+
// A manual retry is a fresh user decision, independent of the automatic
|
|
4558
|
+
// two-attempt budget. Keep the selected agent/model and same conversation.
|
|
4559
|
+
goal.autoRework = { ...(goal.autoRework ?? {}), testFailures: 0, manualRetryAt: new Date().toISOString() };
|
|
4560
|
+
saveGoal(goal);
|
|
4561
|
+
const task = queueReplyTask(goal, prompt, { from: 'manager', via: 'retry' });
|
|
4562
|
+
return json(res, 200, { goal, task, repairing: true });
|
|
4563
|
+
}
|
|
4538
4564
|
const wdir = (goalWorkDirs.get(goal.id) && existsSync(goalWorkDirs.get(goal.id))) ? goalWorkDirs.get(goal.id) : project.dir;
|
|
4539
4565
|
// 再検証 is the explicit human "run the tests now" — force the gate on for this
|
|
4540
4566
|
// one goal even when the default (automatic) gate is off.
|
package/package.json
CHANGED