@galda/cli 0.10.114 → 0.10.115
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/engine/lib.mjs +16 -15
- package/engine/server.mjs +74 -31
- package/package.json +1 -1
package/engine/lib.mjs
CHANGED
|
@@ -897,7 +897,14 @@ export function parseStreamEvents(line) {
|
|
|
897
897
|
const arg = i.file_path ?? i.path ?? i.pattern ?? i.command ?? i.url ?? i.query ?? '';
|
|
898
898
|
out.push({ kind: 'activity', text: `${p.name} ${String(arg)}`.trim().slice(0, 140) });
|
|
899
899
|
} else if (p.type === 'text' && p.text?.trim()) {
|
|
900
|
-
|
|
900
|
+
// The worker talking, as opposed to the worker working. It was flattened
|
|
901
|
+
// into the same ✎-prefixed, 110-char activity line as every tool call,
|
|
902
|
+
// which is why a person who sent a request sat watching a tool log with
|
|
903
|
+
// nothing in it that reads as an answer (Masa's friend, 2026-07-31: three
|
|
904
|
+
// minutes and "no reply"). Kept whole and kept separate here; the caller
|
|
905
|
+
// still writes the short activity line, and can also show the first one as
|
|
906
|
+
// what it is — the agent's first words back.
|
|
907
|
+
out.push({ kind: 'say', text: p.text.trim() });
|
|
901
908
|
}
|
|
902
909
|
}
|
|
903
910
|
return out;
|
|
@@ -4858,20 +4865,14 @@ export function shouldEmitSetupCompleted({ isMcpClient, alreadySeen, flagExists
|
|
|
4858
4865
|
return Boolean(isMcpClient) && !alreadySeen && !flagExists;
|
|
4859
4866
|
}
|
|
4860
4867
|
|
|
4861
|
-
//
|
|
4862
|
-
//
|
|
4863
|
-
//
|
|
4864
|
-
//
|
|
4865
|
-
//
|
|
4866
|
-
//
|
|
4867
|
-
//
|
|
4868
|
-
//
|
|
4869
|
-
export function needsProjectFolder({ dir, homeDir, isRepo } = {}) {
|
|
4870
|
-
if (!dir) return true;
|
|
4871
|
-
const norm = (p) => String(p).replace(/\/+$/, '');
|
|
4872
|
-
if (homeDir && norm(dir) === norm(homeDir)) return true;
|
|
4873
|
-
return !isRepo;
|
|
4874
|
-
}
|
|
4868
|
+
// `needsProjectFolder` lived here until 2026-07-31. It decided, before running,
|
|
4869
|
+
// that a folder was not good enough to work in (HOME, or not a git repo) and
|
|
4870
|
+
// that the person had to answer a question first. Masa removed the question:
|
|
4871
|
+
// `claude -p` in a plain folder simply works — measured — so Galda asking is a
|
|
4872
|
+
// toll gate of our own invention, and the folder is now whatever the CLI was
|
|
4873
|
+
// started in, changeable on the composer chip by whoever cares. The builders
|
|
4874
|
+
// below stay: an install that was mid-question when it updated must still be
|
|
4875
|
+
// able to answer the one it already has.
|
|
4875
4876
|
|
|
4876
4877
|
// The repo's own name: the last segment of its path. The composer chip prints this and
|
|
4877
4878
|
// the folder menu prints it again on every row, so it lives here as one function — two
|
package/engine/server.mjs
CHANGED
|
@@ -20,7 +20,7 @@ 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 {
|
|
23
|
+
import { folderLabel, chooseFolderScript, parseChosenFolder, buildFolderQuestion, resolveFolderAnswer, classifyFolderTarget, buildFolderInitConfirm, isFolderInitConfirmed, needsReviewPreference, buildReviewPreferenceQuestion, resolveReviewPreferenceAnswer, routeQuestionAnswer, notUnderstoodNote, parseRelocalizedQuestion, classifyAuthProbe, authBannerText, makeSigninChallenge, parseClaimResponse, isCommandOnPath, hostReadiness, parseRequestedWorkspaceDir } from './lib.mjs';
|
|
24
24
|
import { parseStreamEvents, parseCodexEvents, buildCodexArgs, parsePlan, refinePlanTasks, canEditGoal, canDeleteGoal, shouldAskClarification, workerExitReason, classifyWorkerFailure, isForcedStop, taskStatusAfterVerify, workerResultText, taskCountsAsComplete, resolveWantsPR, resolveGoalSource, buildGoalSummary, buildReviewAttemptLedger, latestGoalOutcomeTask, buildDirtyWorkspacePrBlock, 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, rebasePreviewUrl, shouldCreateGoalPR, normalizePullRequestUrl, 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, buildQueueWaits, nextRunnableTasks, goalsConflict, detectConflicts, pickFoldTarget, autoFoldReviewsEnabled, parseGitUnifiedDiffLocations, hasTestRelevantChanges, parseFailingTestNames, classifyTestGate, classifyPrSafety, isInconclusiveTestRun, countInfraFlakes, classifyRunFailures, classifyVerifyGate, buildVerificationInfraError, verificationInfraErrorFrom, 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, isNothingVerifiableForPR, isSuspiciouslyIncompleteDone, classifyComposerIntentHeuristic, detectPauseIntent, buildIntentPrompt, parseIntentResponse, buildBoardSnapshot, validateBoardSnapshot, boardIsEmpty, decideBoardPull, resolveConnectedAgents, pickAvailableAgent, pickUtilityAgent, utilityModel, liveTakeoverDecision, shouldOpenBrowserOnBoot, goalTaskTitle, buildGoalTask, shouldUsePlannerForGoal, RUN_DECL_FILE, REQUIREMENT_EVIDENCE_FILE, parseRequirementEvidenceDocument, parseRunDeclaration, SHOT_DIR, collectShotNames, parseGoalAddress, REPLY_OUTCOMES, buildGoalMessage, serializeGoalMessage, parseGoalMessages, computeInternalQualityMetrics, classifyInternalQualityTaskError, resolveWorkspaceMode, goalSessionFor, setGoalAgentSession, switchGoalAgentSession, parseApprovalRequest } from './lib.mjs';
|
|
25
25
|
import { migrateRequirementModel, validateRequirementModel } from './requirement-model.mjs';
|
|
26
26
|
import { buildManagerVerificationChecks, buildRequirementVerificationEvidence, mergeRequirementVerificationEvidence } from './requirement-verification.mjs';
|
|
@@ -958,6 +958,26 @@ function addGoalMessage(goal, { from, via, text, at } = {}) {
|
|
|
958
958
|
saveGoal(goal);
|
|
959
959
|
return msg;
|
|
960
960
|
}
|
|
961
|
+
// The agent's first words back, put where a person looks for an answer.
|
|
962
|
+
//
|
|
963
|
+
// A request used to produce nothing that reads as a reply until the run ended —
|
|
964
|
+
// minutes later. The output was never missing (every tool call streams into the
|
|
965
|
+
// run log), but the run log is a log: `✎ …` lines clipped to 110 characters,
|
|
966
|
+
// interleaved with `Edit app.js`. Masa's friend sent one message and waited three
|
|
967
|
+
// minutes for something that never came, because the thing that came did not look
|
|
968
|
+
// like an answer.
|
|
969
|
+
//
|
|
970
|
+
// So the FIRST thing the worker says on a run is recorded as its message on the
|
|
971
|
+
// goal, verbatim — the same place its final report already lands. Galda writes
|
|
972
|
+
// nothing of its own here and picks nothing: what to open with is the agent's
|
|
973
|
+
// choice, and this only carries it.
|
|
974
|
+
const workerOpenedWith = new Set();
|
|
975
|
+
function sayFromWorker(goal, task, text) {
|
|
976
|
+
if (!goal || !task || workerOpenedWith.has(task.id)) return;
|
|
977
|
+
workerOpenedWith.add(task.id);
|
|
978
|
+
addGoalMessage(goal, { from: 'ai', via: task.reply ? 'thread' : 'composer', text });
|
|
979
|
+
}
|
|
980
|
+
|
|
961
981
|
function readGoalMessages(goalId) {
|
|
962
982
|
try { return parseGoalMessages(readFileSync(goalMessagesFile(goalId), 'utf8')); }
|
|
963
983
|
catch { return []; }
|
|
@@ -1271,7 +1291,7 @@ function sendTodos(t, todos) {
|
|
|
1271
1291
|
}
|
|
1272
1292
|
|
|
1273
1293
|
// ---- claude helpers --------------------------------------------------------
|
|
1274
|
-
function runClaude({ prompt, cwd, tools, onEvent, onTodos, resume, model, effort, permissionMode = 'acceptEdits', onChild }) {
|
|
1294
|
+
function runClaude({ prompt, cwd, tools, onEvent, onTodos, onSay, resume, model, effort, permissionMode = 'acceptEdits', onChild }) {
|
|
1275
1295
|
return new Promise((done) => {
|
|
1276
1296
|
const args = [
|
|
1277
1297
|
'-p', prompt,
|
|
@@ -1299,6 +1319,13 @@ function runClaude({ prompt, cwd, tools, onEvent, onTodos, resume, model, effort
|
|
|
1299
1319
|
if (ev.kind === 'result') { result = ev.text; usage = ev.usage; }
|
|
1300
1320
|
else if (ev.kind === 'session') sessionId = ev.id;
|
|
1301
1321
|
else if (ev.kind === 'todos') onTodos?.(ev.todos);
|
|
1322
|
+
else if (ev.kind === 'say') {
|
|
1323
|
+
// Two things from one event: the short line the run log has always
|
|
1324
|
+
// shown, and the words themselves, so the caller can put the first
|
|
1325
|
+
// ones in front of the person as an actual reply.
|
|
1326
|
+
const short = `✎ ${ev.text.replace(/\s+/g, ' ').slice(0, 110)}`;
|
|
1327
|
+
lastAct = short; onEvent?.(short); onSay?.(ev.text);
|
|
1328
|
+
}
|
|
1302
1329
|
else { if (ev.text?.trim()) lastAct = ev.text.trim(); onEvent?.(ev.text); }
|
|
1303
1330
|
}
|
|
1304
1331
|
}
|
|
@@ -1314,7 +1341,7 @@ function runClaude({ prompt, cwd, tools, onEvent, onTodos, resume, model, effort
|
|
|
1314
1341
|
// The Codex half of the worker bridge. Same contract as runClaude, because
|
|
1315
1342
|
// nothing downstream may care which agent ran: live activity, the agent's own
|
|
1316
1343
|
// to-dos (the board), the thread id to talk to it again, usage, final message.
|
|
1317
|
-
function runCodex({ prompt, cwd, onEvent, onTodos, resume, permissionMode = 'acceptEdits', model, effort, images, onChild }) {
|
|
1344
|
+
function runCodex({ prompt, cwd, onEvent, onTodos, onSay, resume, permissionMode = 'acceptEdits', model, effort, images, onChild }) {
|
|
1318
1345
|
return new Promise((done) => {
|
|
1319
1346
|
const sandbox = permissionMode === 'plan' ? 'read-only' : 'workspace-write';
|
|
1320
1347
|
const args = buildCodexArgs({ model: workerModel('codex', model), effort: workerEffort('codex', effort), sandbox, cwd, resume, images });
|
|
@@ -1343,6 +1370,7 @@ function runCodex({ prompt, cwd, onEvent, onTodos, resume, permissionMode = 'acc
|
|
|
1343
1370
|
else if (ev.kind === 'message') {
|
|
1344
1371
|
result = ev.text;
|
|
1345
1372
|
onEvent?.(`✎ ${ev.text.trim().replace(/\s+/g, ' ').slice(0, 500)}`);
|
|
1373
|
+
onSay?.(ev.text);
|
|
1346
1374
|
} else if (ev.kind === 'error') {
|
|
1347
1375
|
err += `${ev.text}\n`;
|
|
1348
1376
|
onEvent?.(`Error ${ev.text}`);
|
|
@@ -3004,6 +3032,7 @@ async function runTask(task) {
|
|
|
3004
3032
|
model: runModel, effort: runEffort, onEvent: (line) => sendAct(task, line),
|
|
3005
3033
|
images: goal?.images,
|
|
3006
3034
|
onTodos: (todos) => sendTodos(task, todos),
|
|
3035
|
+
onSay: (text) => sayFromWorker(goal, task, text),
|
|
3007
3036
|
onChild: (c) => trackGoalWorker(goal?.id, c),
|
|
3008
3037
|
});
|
|
3009
3038
|
if (goal?.cancelled) return; // goal deleted mid-run → don't resurrect the task / run the gate
|
|
@@ -3093,6 +3122,7 @@ async function runTask(task) {
|
|
|
3093
3122
|
model: runModel, effort: runEffort, onEvent: (line) => sendAct(task, line),
|
|
3094
3123
|
images: goal?.images,
|
|
3095
3124
|
onTodos: (todos) => sendTodos(task, todos),
|
|
3125
|
+
onSay: (text) => sayFromWorker(goal, task, text),
|
|
3096
3126
|
onChild: (c) => trackGoalWorker(goal?.id, c),
|
|
3097
3127
|
});
|
|
3098
3128
|
if (goal?.cancelled) return; // deleted mid-run → stop, don't run verify/PR
|
|
@@ -3122,6 +3152,7 @@ async function runTask(task) {
|
|
|
3122
3152
|
model: runModel, effort: runEffort, onEvent: (line) => sendAct(task, line),
|
|
3123
3153
|
images: goal?.images,
|
|
3124
3154
|
onTodos: (todos) => sendTodos(task, todos),
|
|
3155
|
+
onSay: (text) => sayFromWorker(goal, task, text),
|
|
3125
3156
|
onChild: (c) => trackGoalWorker(goal?.id, c),
|
|
3126
3157
|
});
|
|
3127
3158
|
if (goal?.cancelled) return;
|
|
@@ -4708,19 +4739,21 @@ const server = createServer(async (req, res) => {
|
|
|
4708
4739
|
try {
|
|
4709
4740
|
const input = JSON.parse(body || '{}');
|
|
4710
4741
|
if (typeof input.name === 'string' && input.name.trim()) project.name = input.name.trim().slice(0, 80);
|
|
4711
|
-
// Moving a project to another folder.
|
|
4712
|
-
//
|
|
4713
|
-
//
|
|
4714
|
-
//
|
|
4715
|
-
//
|
|
4742
|
+
// Moving a project to another folder. The only thing refused is a folder
|
|
4743
|
+
// that is not there — a path with nothing behind it is a typo, and every
|
|
4744
|
+
// later failure would be reported somewhere far away from the mistake.
|
|
4745
|
+
//
|
|
4746
|
+
// "Not a git repository" used to be refused here too. It is not our call
|
|
4747
|
+
// (Masa 2026-07-31): `claude -p` in a plain folder simply works, and a
|
|
4748
|
+
// person who points Galda at one has said what they want. Such a folder
|
|
4749
|
+
// gets no isolated worktree and can produce no pull request; that is a
|
|
4750
|
+
// property of the folder, said where those things happen, not a reason to
|
|
4751
|
+
// reject the choice.
|
|
4716
4752
|
if (input.dir !== undefined) {
|
|
4717
4753
|
const dir = String(input.dir ?? '').trim();
|
|
4718
4754
|
if (!dir) return json(res, 400, { error: 'folder is required' });
|
|
4719
4755
|
const abs = dir.startsWith('~') ? join(HOME, dir.slice(1)) : resolve(dir);
|
|
4720
4756
|
if (!existsSync(abs)) return json(res, 400, { error: 'no such folder', dir: abs });
|
|
4721
|
-
if (needsProjectFolder({ dir: abs, homeDir: HOME, isRepo: isGitRepo(abs) })) {
|
|
4722
|
-
return json(res, 400, { error: 'not a git repository', dir: abs });
|
|
4723
|
-
}
|
|
4724
4757
|
project.dir = abs;
|
|
4725
4758
|
}
|
|
4726
4759
|
saveProjects();
|
|
@@ -4859,24 +4892,30 @@ const server = createServer(async (req, res) => {
|
|
|
4859
4892
|
// no PR) — the reviewer just Approves (files it) or Dismisses (drops
|
|
4860
4893
|
// it). This is how "needs my judgment" reaches the phone without kicking
|
|
4861
4894
|
// off an implementation run.
|
|
4862
|
-
//
|
|
4863
|
-
//
|
|
4864
|
-
//
|
|
4865
|
-
//
|
|
4866
|
-
//
|
|
4867
|
-
//
|
|
4868
|
-
//
|
|
4869
|
-
//
|
|
4895
|
+
// No folder question (Masa 2026-07-31: "フォルダは先に聞かないでいい…選びたい
|
|
4896
|
+
// 人が選べばいい", and asked for parity with Claude Code, which was then
|
|
4897
|
+
// measured: `claude -p` in a non-git folder just runs — it never asks which
|
|
4898
|
+
// folder and never git-inits one).
|
|
4899
|
+
//
|
|
4900
|
+
// The first thing a new person met used to be an interrogation: which
|
|
4901
|
+
// folder, then whether they wanted PRs, before a single word of their
|
|
4902
|
+
// request was acted on. The folder half is gone. The folder is whatever
|
|
4903
|
+
// the CLI was started in, shown on the composer chip, and changeable
|
|
4904
|
+
// there by anyone who cares — which is the only place it was ever a real
|
|
4905
|
+
// choice rather than a toll gate.
|
|
4906
|
+
//
|
|
4907
|
+
// The old worry (a non-repo folder means the worker edits nothing and we
|
|
4908
|
+
// still say "done") is not answered by asking, and never was: goalWorkDir
|
|
4909
|
+
// already falls back to the project folder itself when there is no git
|
|
4910
|
+
// repo to cut a worktree from, so the work happens where the person is
|
|
4911
|
+
// looking, exactly as Claude Code does. What such a folder cannot do is
|
|
4912
|
+
// produce a pull request, and that is stated where PRs are decided, not
|
|
4913
|
+
// guarded here.
|
|
4870
4914
|
const runnable = !pending && !review && !approval;
|
|
4871
|
-
const needsFolder = runnable && needsProjectFolder({ dir: project.dir, homeDir: HOME, isRepo: isGitRepo(project.dir) });
|
|
4872
4915
|
const lang = /[-ヿ㐀-鿿]/.test(text) ? 'ja' : 'en';
|
|
4873
|
-
|
|
4874
|
-
//
|
|
4875
|
-
|
|
4876
|
-
// PRs. Skipped this round if a folder question already claimed the
|
|
4877
|
-
// needsInput slot; that goal gets rebound to a real repo via /answer
|
|
4878
|
-
// and the NEXT goal for that project is what actually asks this.
|
|
4879
|
-
const needsReviewPref = runnable && !needsFolder && needsReviewPreference({ askedBefore: project.reviewPrefAsked });
|
|
4916
|
+
// Task 5 (PR運用ヒアリング): ask — once per project, before its first
|
|
4917
|
+
// runnable goal — whether this project wants PRs.
|
|
4918
|
+
const needsReviewPref = runnable && needsReviewPreference({ askedBefore: project.reviewPrefAsked });
|
|
4880
4919
|
const reviewPrefQuestion = needsReviewPref ? buildReviewPreferenceQuestion({ lang }) : undefined;
|
|
4881
4920
|
if (needsReviewPref) { project.reviewPrefAsked = true; saveProjects(); }
|
|
4882
4921
|
// Clamp to whatever agent CLIs are actually installed on this machine
|
|
@@ -4892,8 +4931,8 @@ const server = createServer(async (req, res) => {
|
|
|
4892
4931
|
if (review && existingPr && !importedPr) return json(res, 400, { error: 'existingPr must be a GitHub pull request URL' });
|
|
4893
4932
|
const goal = {
|
|
4894
4933
|
id: nextId++, projectId, text: text.trim().slice(0, 8000),
|
|
4895
|
-
status:
|
|
4896
|
-
question:
|
|
4934
|
+
status: needsReviewPref ? 'needsInput' : approval ? 'needsApproval' : review ? 'review' : pending ? 'pending' : 'stacked',
|
|
4935
|
+
question: reviewPrefQuestion,
|
|
4897
4936
|
pauseReason: approval ? 'approval_required' : undefined,
|
|
4898
4937
|
approvalRequest: approval ? {
|
|
4899
4938
|
type: 'approval_required',
|
|
@@ -4947,7 +4986,7 @@ const server = createServer(async (req, res) => {
|
|
|
4947
4986
|
// pending = deliberately shelved (Phase 2 material): never planned,
|
|
4948
4987
|
// never queued, until POST /api/goals/:id/activate. review-only items
|
|
4949
4988
|
// are terminal-until-human too, so they also skip planGoal.
|
|
4950
|
-
if (runnable && !
|
|
4989
|
+
if (runnable && !needsReviewPref) startNextStacked(goal.projectId);
|
|
4951
4990
|
json(res, 200, goal);
|
|
4952
4991
|
} catch { json(res, 400, { error: 'bad json' }); }
|
|
4953
4992
|
});
|
|
@@ -6027,7 +6066,11 @@ async function answerGoalQuestion(goal, question) {
|
|
|
6027
6066
|
const handoffText = crossProject ? [
|
|
6028
6067
|
`[Cross-project implementation handoff from #${goal.id}]`,
|
|
6029
6068
|
`Original request: ${goal.text}`,
|
|
6030
|
-
|
|
6069
|
+
// `.changed` is the reviewed result — summarizeForReview returns
|
|
6070
|
+
// {title, check, changed} and never a `.summary`, so this line read a
|
|
6071
|
+
// key that has never existed and dropped out of every handoff. The
|
|
6072
|
+
// digest builder above already spells it `g.reviewSummary?.changed`.
|
|
6073
|
+
goal.reviewSummary?.changed ? `Reviewed result: ${goal.reviewSummary.changed}` : '',
|
|
6031
6074
|
latest?.result ? `Latest worker result: ${String(latest.result).slice(0, 3000)}` : '',
|
|
6032
6075
|
`User's implementation request: ${text}`,
|
|
6033
6076
|
].filter(Boolean).join('\n\n') : text;
|
package/package.json
CHANGED