@galda/cli 0.10.11 → 0.10.13
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 +12 -5
- package/engine/relay-client.mjs +12 -1
- package/engine/server.mjs +56 -31
- package/package.json +1 -1
package/app/index.html
CHANGED
|
@@ -5814,12 +5814,19 @@ function bootDisconnected(info){
|
|
|
5814
5814
|
const cw = document.getElementById('composerWrap');
|
|
5815
5815
|
if (cw) cw.addEventListener('mousedown', (e) => { if (e.target.closest('#appsel, #modelsel, #effortsel')) return; gate(e); });
|
|
5816
5816
|
|
|
5817
|
-
//
|
|
5818
|
-
//
|
|
5817
|
+
// Come alive automatically: while this device's engine is offline the relay
|
|
5818
|
+
// 503s every engine-backed path (only /, /theme.css, /fonts.css are served);
|
|
5819
|
+
// the instant the agent connects, the same path is proxied to the live engine
|
|
5820
|
+
// (200). So we probe an engine-only endpoint by STATUS and reload when it stops
|
|
5821
|
+
// 503-ing. We must NOT decide this by fetching `/` and grepping its HTML for a
|
|
5822
|
+
// sentinel: this very poll's source contains the string '__GALDA_DISCONNECTED',
|
|
5823
|
+
// so it is baked into every proxied copy of the page — the match is ALWAYS
|
|
5824
|
+
// "found", the reload never fires, and the tab stays stuck on the connect
|
|
5825
|
+
// screen forever ("npx said done but nothing happens"). (fix: status, not body)
|
|
5819
5826
|
setInterval(() => {
|
|
5820
|
-
fetch('/', { cache: 'no-store', credentials: 'same-origin' })
|
|
5821
|
-
if (
|
|
5822
|
-
|
|
5827
|
+
fetch('/api/state', { cache: 'no-store', credentials: 'same-origin' })
|
|
5828
|
+
.then((r) => { if (r.status !== 503) location.reload(); })
|
|
5829
|
+
.catch(() => {});
|
|
5823
5830
|
}, 4000);
|
|
5824
5831
|
}
|
|
5825
5832
|
|
package/engine/relay-client.mjs
CHANGED
|
@@ -100,8 +100,19 @@ function connect(retryMs = 1000) {
|
|
|
100
100
|
live.delete(m.id);
|
|
101
101
|
}
|
|
102
102
|
};
|
|
103
|
-
ws.onclose = () => {
|
|
103
|
+
ws.onclose = (ev) => {
|
|
104
104
|
if (ws !== currentWs) return; // a superseded socket (we already moved on) — don't reconnect
|
|
105
|
+
// Close 4000 = the relay evicted us because a NEWER agent for the SAME account
|
|
106
|
+
// connected (server.mjs "replaced by a newer connection"). If we reconnect we
|
|
107
|
+
// evict THAT one → the two instances mutual-kick forever, which surfaces to the
|
|
108
|
+
// user as app.galda.app reload-looping. Stand down: the newest instance is
|
|
109
|
+
// authoritative. (A normal network drop uses a different code and still
|
|
110
|
+
// reconnects; sign-in via the watcher below can still retake.)
|
|
111
|
+
if (ev && ev.code === 4000) {
|
|
112
|
+
console.log('[agent] relay: superseded by a newer instance of this account — standing down (run only one manager per account)');
|
|
113
|
+
currentWs = null;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
105
116
|
console.log(`[agent] relay connection lost — retrying in ${retryMs}ms`);
|
|
106
117
|
scheduleReconnect(retryMs);
|
|
107
118
|
};
|
package/engine/server.mjs
CHANGED
|
@@ -20,8 +20,8 @@ import { resolve, dirname, join, basename } from 'node:path';
|
|
|
20
20
|
import { homedir } from 'node:os';
|
|
21
21
|
import { fileURLToPath } from 'node:url';
|
|
22
22
|
import { pathToFileURL } from 'node:url';
|
|
23
|
-
import { needsProjectFolder, folderLabel, buildFolderQuestion, resolveFolderAnswer } from './lib.mjs';
|
|
24
|
-
import { parseStreamEvents, parsePlan, refinePlanTasks, findOverlappingGoal, canEditGoal, canDeleteGoal, shouldAskClarification, workerExitReason, isForcedStop, workerResultText, taskCountsAsComplete, resolveWantsPR, resolveGoalSource, buildGoalSummary, buildGoalProofMd,
|
|
23
|
+
import { needsProjectFolder, folderLabel, buildFolderQuestion, resolveFolderAnswer, needsReviewPreference, buildReviewPreferenceQuestion, resolveReviewPreferenceAnswer } from './lib.mjs';
|
|
24
|
+
import { parseStreamEvents, parsePlan, refinePlanTasks, findOverlappingGoal, canEditGoal, canDeleteGoal, shouldAskClarification, workerExitReason, isForcedStop, workerResultText, taskCountsAsComplete, resolveWantsPR, resolveGoalSource, buildGoalSummary, buildGoalProofMd, buildGoalPrBody, nextGoalStatus, isPrMerged, isPrApproved, reviewToDoneStatus, advanceReviewGoal, revertReviewGoal, approveGoal, dismissGoal, 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, buildEphemeralSeedLog, buildEphemeralSeedProjects, trimTaskActivityForState, buildAskContext, WORKER_TOOLS, verifyCfAccessJwt, resolveIdentity, goalVisibleTo, clampParallelLimit, canStartMore, nextRunnableTasks, goalsConflict, detectConflicts, isUiChange, shouldCaptureProof, shouldCaptureBaseline, hasTestRelevantChanges, buildExecutionPlan, buildContextHandoffSummary, buildFailurePostmortem, appendFailureMemory, latestFailurePolicy, classifyChangeRisk, checkRunBudget, usageBudgetTokens, isRateLimited, nextResumeDelay, checkFreeTierLimit, resolveEntitlement, resolveCachedEntitlement, FREE_TIER_LIMITS, verifyLicenseToken, shouldEmitSetupCompleted, pickAnalyticsUid, shouldEmitFreeExhausted, detectRequestLanguage, isNothingVerifiable } from './lib.mjs';
|
|
25
25
|
import { openPR } from './pr.mjs';
|
|
26
26
|
import { runVerification, exerciseUi } from './verify.mjs';
|
|
27
27
|
|
|
@@ -2160,6 +2160,11 @@ async function finishGoalIfComplete(goal, project, activityTask = null) {
|
|
|
2160
2160
|
// point (all its tasks are done), so its conflictsWith is accurate even if
|
|
2161
2161
|
// PR creation itself fails (e.g. no `gh` auth) or is still no-op'd (no code).
|
|
2162
2162
|
updateGoalConflicts(goal);
|
|
2163
|
+
// Computed BEFORE createGoalPR (not inside verifyGate, which used to run
|
|
2164
|
+
// after) so the PR body's "確認ポイント" headline is available the first
|
|
2165
|
+
// time the PR is opened, not only on the review card (Masa 2026-07-16).
|
|
2166
|
+
goalAct('Writing the review summary from the worker report and diff.');
|
|
2167
|
+
goal.reviewSummary = await summarizeForReview(goal, siblings, wdir, reviewDefinition);
|
|
2163
2168
|
goalAct('Checking changed files and preparing the PR branch if code changed.');
|
|
2164
2169
|
await createGoalPR(goal, prProject, siblings, reviewDefinition, activityTask);
|
|
2165
2170
|
// goal 427 §4: classify the change's risk from the SAME changed-files/diff
|
|
@@ -2228,8 +2233,13 @@ async function verifyGate(goal, prProject, siblings, baseProject, activityTask =
|
|
|
2228
2233
|
goal.testResult = { ran: false, skipped: true, skippedReason: changedFiles.length ? 'documentation-only change' : 'no changed files' };
|
|
2229
2234
|
goalAct(`Skipped project tests: ${goal.testResult.skippedReason}.`);
|
|
2230
2235
|
}
|
|
2231
|
-
|
|
2232
|
-
|
|
2236
|
+
// Normally already set by finishGoalIfComplete before the PR was opened
|
|
2237
|
+
// (so the PR body can include it too); only fall back here for the
|
|
2238
|
+
// standalone /reverify path or any older goal that never got one.
|
|
2239
|
+
if (!goal.reviewSummary) {
|
|
2240
|
+
goalAct('Writing the review summary from the worker report and diff.');
|
|
2241
|
+
goal.reviewSummary = await summarizeForReview(goal, siblings, wdir, reviewDefinition);
|
|
2242
|
+
}
|
|
2233
2243
|
const uiTasks = siblings.filter((t) => isUiChange(t.changedFiles));
|
|
2234
2244
|
// Only require/capture proof for projects we can actually boot & screenshot
|
|
2235
2245
|
// (Manager-shaped). A generic web repo that edits a stylesheet can't be
|
|
@@ -2371,26 +2381,12 @@ async function createGoalPR(goal, project, goalTasks, reviewDefinition = DEFAULT
|
|
|
2371
2381
|
// (Masa: 「ユーザーの言語で」).
|
|
2372
2382
|
const lang = reviewDefinition.language === 'auto' ? detectRequestLanguage(goal.text) : (reviewDefinition.language || 'ja');
|
|
2373
2383
|
const L = (en, ja) => (lang === 'en' ? en : ja);
|
|
2374
|
-
const bodyFor = (branch) =>
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
] : []),
|
|
2381
|
-
L('## Goal', '## ゴール'),
|
|
2382
|
-
'',
|
|
2383
|
-
goal.text.slice(0, 1500),
|
|
2384
|
-
'',
|
|
2385
|
-
...buildReviewRuleSection(reviewDefinition.description),
|
|
2386
|
-
L(`Implemented by Manager for AI: the goal was decomposed into ${goalTasks.length} task(s), each executed by a Claude Code worker (flat-rate, no extra API), tests written and run per task.${proofItems.length ? ' Tasks with a pass condition were verified independently in a headless browser.' : ''}`,
|
|
2387
|
-
`Manager for AI が実装: ゴールを${goalTasks.length}個のタスクに分解し、Claude Code worker が順に実行(既存サブスク・追加API無し・タスク毎にテストを作成し実行)。${proofItems.length ? '成功条件つきタスクは headless ブラウザで独立検証済み。' : ''}`),
|
|
2388
|
-
'',
|
|
2389
|
-
L('## Tasks and worker reports', '## タスクとworker報告'),
|
|
2390
|
-
...goalTasks.map((t) => `\n### ${t.num} ${t.title} — ${t.status} (${t.secs}s)\n${(t.result ?? '').slice(0, 800)}`),
|
|
2391
|
-
'',
|
|
2392
|
-
L(`Changed files: ${files.map((f) => `\`${f}\``).join(' ')}`, `変更ファイル: ${files.map((f) => `\`${f}\``).join(' ')}`),
|
|
2393
|
-
].join('\n');
|
|
2384
|
+
const bodyFor = (branch) => buildGoalPrBody({
|
|
2385
|
+
lang, goal, goalTasks, files, owner, branch, proofRel,
|
|
2386
|
+
proofItemsCount: proofItems.length,
|
|
2387
|
+
reviewDescription: reviewDefinition.description,
|
|
2388
|
+
reviewSummary: goal.reviewSummary,
|
|
2389
|
+
});
|
|
2394
2390
|
const title = `Manager for AI: ${isRework ? L('rework — ', '再作業 — ') : ''}${goal.plan?.[0] ?? goal.text.slice(0, 60)}${goal.plan?.length > 1 ? L(` +${goal.plan.length - 1} more`, ` 他${goal.plan.length - 1}件`) : ''}`;
|
|
2395
2391
|
const { url, branch } = openPR({
|
|
2396
2392
|
repoDir: project.dir, slug: `goal-${goal.id}`,
|
|
@@ -2401,7 +2397,7 @@ async function createGoalPR(goal, project, goalTasks, reviewDefinition = DEFAULT
|
|
|
2401
2397
|
extraCopies,
|
|
2402
2398
|
extraWrites: proofItems.length ? [{
|
|
2403
2399
|
destRel: `${proofRel}/PROOF.md`,
|
|
2404
|
-
content: buildGoalProofMd({ goalText: goal.text, items: proofItems }),
|
|
2400
|
+
content: buildGoalProofMd({ goalText: goal.text, items: proofItems, lang }),
|
|
2405
2401
|
}] : [],
|
|
2406
2402
|
...(isRework ? { existingBranch: goal.prBranch, existingPrUrl: goal.pr } : {}),
|
|
2407
2403
|
});
|
|
@@ -2750,15 +2746,22 @@ const server = createServer(async (req, res) => {
|
|
|
2750
2746
|
// for pending/review items (they never spawn a worker).
|
|
2751
2747
|
const runnable = !pending && !review;
|
|
2752
2748
|
const needsFolder = runnable && needsProjectFolder({ dir: project.dir, homeDir: HOME, isRepo: isGitRepo(project.dir) });
|
|
2753
|
-
const
|
|
2754
|
-
|
|
2755
|
-
|
|
2749
|
+
const lang = /[-ヿ㐀-鿿]/.test(text) ? 'ja' : 'en';
|
|
2750
|
+
const folderQuestion = needsFolder ? buildFolderQuestion(detectRepos(HOME), { lang }) : undefined;
|
|
2751
|
+
// Task 5 (PR運用ヒアリング): once the folder is settled, ask — once per
|
|
2752
|
+
// project, before its first runnable goal — whether this project wants
|
|
2753
|
+
// PRs. Skipped this round if a folder question already claimed the
|
|
2754
|
+
// needsInput slot; that goal gets rebound to a real repo via /answer
|
|
2755
|
+
// and the NEXT goal for that project is what actually asks this.
|
|
2756
|
+
const needsReviewPref = runnable && !needsFolder && needsReviewPreference({ askedBefore: project.reviewPrefAsked });
|
|
2757
|
+
const reviewPrefQuestion = needsReviewPref ? buildReviewPreferenceQuestion({ lang }) : undefined;
|
|
2758
|
+
if (needsReviewPref) { project.reviewPrefAsked = true; saveProjects(); }
|
|
2756
2759
|
const goalAgent = workerAgent(agent);
|
|
2757
2760
|
const priorFailureMemory = recentProjectFailureMemory(projectId, text.trim());
|
|
2758
2761
|
const goal = {
|
|
2759
2762
|
id: nextId++, projectId, text: text.trim().slice(0, 8000),
|
|
2760
|
-
status: needsFolder ? 'needsInput' : review ? 'review' : pending ? 'pending' : 'planning',
|
|
2761
|
-
question: folderQuestion,
|
|
2763
|
+
status: (needsFolder || needsReviewPref) ? 'needsInput' : review ? 'review' : pending ? 'pending' : 'planning',
|
|
2764
|
+
question: folderQuestion || reviewPrefQuestion,
|
|
2762
2765
|
reviewOnly: review ? true : undefined,
|
|
2763
2766
|
note: review && typeof note === 'string' && note.trim() ? note.trim().slice(0, 8000) : undefined,
|
|
2764
2767
|
wantsPR: review ? false : resolveWantsPR(pr, text, getReviewDefinition(projectId).defaultWantsPR),
|
|
@@ -2788,7 +2791,7 @@ const server = createServer(async (req, res) => {
|
|
|
2788
2791
|
// pending = deliberately shelved (Phase 2 material): never planned,
|
|
2789
2792
|
// never queued, until POST /api/goals/:id/activate. review-only items
|
|
2790
2793
|
// are terminal-until-human too, so they also skip planGoal.
|
|
2791
|
-
if (runnable && !needsFolder) {
|
|
2794
|
+
if (runnable && !needsFolder && !needsReviewPref) {
|
|
2792
2795
|
planGoal(goal).catch((e) => {
|
|
2793
2796
|
goal.status = 'failed'; goal.prError = String(e).slice(0, 300); saveGoal(goal);
|
|
2794
2797
|
});
|
|
@@ -3115,6 +3118,28 @@ const server = createServer(async (req, res) => {
|
|
|
3115
3118
|
saveGoal(goal);
|
|
3116
3119
|
return json(res, 200, goal);
|
|
3117
3120
|
}
|
|
3121
|
+
// Review-preference answer (task 5 onboarding funnel): fold into the
|
|
3122
|
+
// project's review definition (defaultWantsPR) rather than goal.text —
|
|
3123
|
+
// like the folder question, this is project setup, not a task detail.
|
|
3124
|
+
if (goal.question?.kind === 'reviewPref') {
|
|
3125
|
+
const project = projects.find((p) => p.id === goal.projectId);
|
|
3126
|
+
const patch = resolveReviewPreferenceAnswer(answer, goal.question);
|
|
3127
|
+
if (project) {
|
|
3128
|
+
const merged = validateReviewDefinition({ ...getReviewDefinition(project.id), ...patch });
|
|
3129
|
+
if (merged.ok) {
|
|
3130
|
+
reviewDefinitions[project.id] = merged.definition;
|
|
3131
|
+
saveReviewDefinitions();
|
|
3132
|
+
send({ ev: 'review-definition', projectId: project.id, reviewDefinition: merged.definition });
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
goal.wantsPR = resolveWantsPR(undefined, goal.text, patch.defaultWantsPR);
|
|
3136
|
+
goal.question = null;
|
|
3137
|
+
goal.clarified = true;
|
|
3138
|
+
goal.status = 'planning';
|
|
3139
|
+
saveGoal(goal);
|
|
3140
|
+
planGoal(goal).catch((e) => { goal.status = 'failed'; goal.prError = String(e).slice(0, 300); saveGoal(goal); });
|
|
3141
|
+
return json(res, 200, goal);
|
|
3142
|
+
}
|
|
3118
3143
|
goal.text = `${goal.text}\n[確認: ${goal.question?.text ?? ''} → ${answer}]`;
|
|
3119
3144
|
goal.clarified = true; // answered once → planGoal must never re-ask (persisted via saveGoal)
|
|
3120
3145
|
goal.question = null;
|
package/package.json
CHANGED