@galda/cli 0.10.65 → 0.10.67

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 CHANGED
@@ -4365,7 +4365,7 @@ function renderStream(){
4365
4365
  if (e.key !== 'Enter' || e.isComposing || !inp.value.trim()) return;
4366
4366
  const text = inp.value.trim(); inp.value = ''; inp.disabled = true;
4367
4367
  const r = await fetch(withKey(`/api/goals/${inp.dataset.goal}/reply`), { method: 'POST',
4368
- headers: { 'content-type': 'application/json' }, body: JSON.stringify({ text }) });
4368
+ headers: { 'content-type': 'application/json' }, body: JSON.stringify(replyWorkerPayload({ text })) });
4369
4369
  if (!r.ok) showErr('Reply failed — the goal may not have started yet.');
4370
4370
  inp.disabled = false;
4371
4371
  });
@@ -4948,7 +4948,7 @@ function openGoalheadReply(id, btn){
4948
4948
  const snd = document.createElement('button'); snd.className = 'qbtn ok'; snd.textContent = '↑';
4949
4949
  wrap.appendChild(inp); wrap.appendChild(snd); head.insertAdjacentElement('afterend', wrap); inp.focus();
4950
4950
  const send = async () => { const t = inp.value.trim(); if (!t) return;
4951
- const r = await fetch(withKey(`/api/goals/${id}/reply`), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ text: t }) });
4951
+ const r = await fetch(withKey(`/api/goals/${id}/reply`), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(replyWorkerPayload({ text: t })) });
4952
4952
  if (!r.ok) showErr('未開始のゴールは編集を使ってください'); await refresh(); };
4953
4953
  inp.onkeydown = (e) => { if (e.key === 'Enter' && !e.isComposing) { e.preventDefault(); send(); } if (e.key === 'Escape') wrap.remove(); };
4954
4954
  snd.onclick = send;
@@ -5405,7 +5405,7 @@ function renderReviewChecklist(){
5405
5405
  b.disabled = true; input.disabled = true;
5406
5406
  const r = await fetch(withKey(`/api/goals/${goalId}/reply`), { method: 'POST',
5407
5407
  headers: { 'content-type': 'application/json' },
5408
- body: JSON.stringify({ text: `[Fix request for requirement #${t?.num ?? ''} "${t?.title ?? ''}"] ${text}` }) });
5408
+ body: JSON.stringify(replyWorkerPayload({ text: `[Fix request for requirement #${t?.num ?? ''} "${t?.title ?? ''}"] ${text}` })) });
5409
5409
  if (r.ok) { input.value = ''; input.placeholder = 'Sent — the worker will pick it up'; }
5410
5410
  else showErr('Failed to send the fix request.');
5411
5411
  b.disabled = false; input.disabled = false;
@@ -5649,9 +5649,12 @@ const REPLY_CHOICE_LABEL = { continue: 'Fix this', rescope: 'Rethink it', split:
5649
5649
  // is the task/workflow vocabulary and this is neither (CDO §5).
5650
5650
  const REPLY_WAIT_ROUTING = 'Thinking…';
5651
5651
  const REPLY_WAIT_ANSWERING = 'Answering…';
5652
+ function replyWorkerPayload(extra = {}){
5653
+ return { ...extra, agent: state.connectedApp, model: state.stickyModel, effort: state.stickyEffort };
5654
+ }
5652
5655
  async function postReply(goalId, text, outcome = 'auto'){
5653
5656
  const r = await fetch(withKey(`/api/goals/${goalId}/dismiss`), { method: 'POST',
5654
- headers: { 'content-type': 'application/json' }, body: JSON.stringify({ text, outcome }) });
5657
+ headers: { 'content-type': 'application/json' }, body: JSON.stringify(replyWorkerPayload({ text, outcome })) });
5655
5658
  if (!r.ok) throw new Error();
5656
5659
  return r.json();
5657
5660
  }
@@ -6139,7 +6142,7 @@ async function sendGoalInstruction(goalId, text){
6139
6142
  $('gdSend').disabled = true; $('gdDiscard').disabled = true;
6140
6143
  try {
6141
6144
  const r = await fetch(withKey(`/api/goals/${goalId}/reply`), { method: 'POST',
6142
- headers: { 'content-type': 'application/json' }, body: JSON.stringify({ text }) });
6145
+ headers: { 'content-type': 'application/json' }, body: JSON.stringify(replyWorkerPayload({ text })) });
6143
6146
  if (!r.ok) throw new Error();
6144
6147
  upsert(state.tasks, await r.json());
6145
6148
  const g = state.goals.find((x) => x.id === goalId);
@@ -7095,7 +7098,7 @@ function renderFsTodo(){
7095
7098
  // hover-toolbar actions → same endpoints as the SL1 goalhead toolbar
7096
7099
  for (const b of el.querySelectorAll('[data-fsreply]')) b.onclick = (e) => { e.stopPropagation();
7097
7100
  fsInlineInput(b.closest('.trow'), '', 'Reply to this goal…', async (text) => {
7098
- const r = await fetch(withKey(`/api/goals/${b.dataset.fsreply}/reply`), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ text }) });
7101
+ const r = await fetch(withKey(`/api/goals/${b.dataset.fsreply}/reply`), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(replyWorkerPayload({ text })) });
7099
7102
  if (!r.ok) showErr('未開始のゴールは編集を使ってください'); await refresh();
7100
7103
  }); };
7101
7104
  for (const b of el.querySelectorAll('[data-fsedit]')) b.onclick = (e) => { e.stopPropagation();
@@ -8577,7 +8580,7 @@ async function send(){
8577
8580
  } else {
8578
8581
  // running/partial/failed/interrupted/blocked → a plain follow-up on the same goal.
8579
8582
  const r = await fetch(withKey(`/api/goals/${bind.goal.id}/reply`), { method: 'POST',
8580
- headers: { 'content-type': 'application/json' }, body: JSON.stringify({ text: fullText }) });
8583
+ headers: { 'content-type': 'application/json' }, body: JSON.stringify(replyWorkerPayload({ text: fullText })) });
8581
8584
  if (!r.ok) throw new Error();
8582
8585
  const g = await r.json(); const goal = g.goal ?? g;
8583
8586
  if (goal?.id) upsert(state.goals, goal);
@@ -9781,7 +9784,7 @@ async function saSendReply(it, i, text){
9781
9784
  it.st = 3; it.rstatus = 'working'; SA.cur = goalId;
9782
9785
  saPaintCard(goalId);
9783
9786
  try {
9784
- const r = await fetch(withKey(`/api/goals/${goalId}/reply`), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ text: workerText }) });
9787
+ const r = await fetch(withKey(`/api/goals/${goalId}/reply`), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(replyWorkerPayload({ text: workerText })) });
9785
9788
  if (!r.ok) throw new Error('reply failed');
9786
9789
  if (isAttn) {
9787
9790
  // A Needs-you reply picks the goal back up → it belongs in Doing, not lingering in
package/engine/lib.mjs CHANGED
@@ -2008,20 +2008,23 @@ export function isPrMerged({ state, mergedAt } = {}) {
2008
2008
  return state === 'MERGED' || !!mergedAt;
2009
2009
  }
2010
2010
 
2011
+ export function isPrClosed({ state, mergedAt } = {}) {
2012
+ return state === 'CLOSED' && !mergedAt;
2013
+ }
2014
+
2011
2015
  // Interpret the same `gh pr view` result's reviewDecision field
2012
2016
  // (APPROVED / CHANGES_REQUESTED / REVIEW_REQUIRED / null).
2013
2017
  export function isPrApproved({ reviewDecision } = {}) {
2014
2018
  return reviewDecision === 'APPROVED';
2015
2019
  }
2016
2020
 
2017
- // A 'review' goal whose PR merged OR was approved is done; every other goal
2018
- // status (and a still-open, unapproved PR) passes through unchanged — this
2021
+ // A 'review' goal whose PR merged OR closed is done; every other goal status
2022
+ // (including an approved but still-open PR) passes through unchanged — this
2019
2023
  // never regresses a goal out of 'review' on its own.
2020
- export function reviewToDoneStatus({ goalStatus, merged }) {
2021
- // Only an actual MERGE closes a review (= the change shipped). A GitHub PR
2022
- // *approval* must NOT auto-Done on reload you review IN the Manager, so an
2023
- // external approval silently clearing your Review was the "勝手にdone" surprise.
2024
- return goalStatus === 'review' && merged ? 'done' : goalStatus;
2024
+ export function reviewToDoneStatus({ goalStatus, merged, closed }) {
2025
+ // Merge and close both end the GitHub review lifecycle. Approval alone does
2026
+ // not: an approved but still-open PR remains actionable in Review.
2027
+ return goalStatus === 'review' && (merged || closed) ? 'done' : goalStatus;
2025
2028
  }
2026
2029
 
2027
2030
  // Build the proof PR body (EN + JA, no emoji) and the PROOF.md committed to
package/engine/server.mjs CHANGED
@@ -23,6 +23,7 @@ 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
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
+ import { isPrClosed } from './lib.mjs';
26
27
  import { collectProjectRules } from './lib.mjs';
27
28
  import { FALLBACK_CODEX_MODELS, loadCodexModels } from './codex-models.mjs';
28
29
  import { CLAUDE_MODELS } from './claude-models.mjs';
@@ -1400,7 +1401,7 @@ async function syncAllExternal() {
1400
1401
  }
1401
1402
 
1402
1403
  // Goals awaiting review carry their PR url in goal.pr; poll `gh pr view` for
1403
- // each and flip 'review' -> 'done' once GitHub reports it merged or approved.
1404
+ // each and flip 'review' -> 'done' once GitHub reports it merged or closed.
1404
1405
  function ghPrView(url) {
1405
1406
  return new Promise((res) => {
1406
1407
  execFile('gh', ['pr', 'view', url, '--json', 'state,mergedAt,reviewDecision'], (e, out, stderr) => {
@@ -1418,13 +1419,15 @@ async function syncGoalMerges() {
1418
1419
  if (!raw) return;
1419
1420
  let data;
1420
1421
  try { data = JSON.parse(raw); } catch { return; }
1421
- const newStatus = reviewToDoneStatus({ goalStatus: g.status, merged: isPrMerged(data), approved: isPrApproved(data) });
1422
+ const merged = isPrMerged(data);
1423
+ const closed = isPrClosed(data);
1424
+ const newStatus = reviewToDoneStatus({ goalStatus: g.status, merged, closed, approved: isPrApproved(data) });
1422
1425
  if (newStatus !== g.status) {
1423
1426
  g.status = newStatus; saveGoal(g);
1424
1427
  // §3/§7: a merged PR is truly terminal (no rework possible) — reclaim the
1425
1428
  // per-goal worktree so .manager-wt/goal-<id> doesn't leak forever. (Manual
1426
1429
  // Approve stays reworkable via revert, so we don't clean those up here.)
1427
- if (newStatus === 'done' && isPrMerged(data)) {
1430
+ if (newStatus === 'done' && (merged || closed)) {
1428
1431
  const project = projects.find((p) => p.id === g.projectId);
1429
1432
  if (project) removeGoalWorkDir(g, project);
1430
1433
  }
@@ -1973,6 +1976,19 @@ function resumeProject(projectId) {
1973
1976
 
1974
1977
  // Slack風スレッド返信タスクの生成(/api/goals/:id/reply と /api/goals/:id/dismiss で共有)。
1975
1978
  // runTask()のtask.reply分岐がgoal.sessionIdをresumeに渡して同じ会話を続ける。
1979
+ function applyReplyWorker(goal, requested = {}) {
1980
+ if (requested.agent == null) return { ok: true };
1981
+ if (!WORKER_AGENTS.includes(requested.agent)) return { ok: false, error: 'unsupported agent' };
1982
+ const agent = workerAgent(requested.agent);
1983
+ if (!AVAILABLE_AGENTS.includes(agent)) return { ok: false, error: `${agent} is not connected on this device` };
1984
+ const switchedAgent = agent !== goal.agent;
1985
+ goal.agent = agent;
1986
+ goal.model = workerModel(agent, requested.model);
1987
+ goal.effort = workerEffort(agent, requested.effort);
1988
+ if (switchedAgent) goal.sessionId = null;
1989
+ return { ok: true };
1990
+ }
1991
+
1976
1992
  function queueReplyTask(goal, text, { from = 'manager', via = 'composer' } = {}) {
1977
1993
  // Every follow-up on a goal is part of its conversation (§1.1). Recorded here
1978
1994
  // rather than at each call site so an internally-generated nudge (auto-retest
@@ -4522,8 +4538,11 @@ const server = createServer(async (req, res) => {
4522
4538
  req.on('data', (d) => { body += d; });
4523
4539
  req.on('end', () => {
4524
4540
  try {
4525
- const { text } = JSON.parse(body);
4541
+ const requested = JSON.parse(body);
4542
+ const { text } = requested;
4526
4543
  if (!text?.trim()) return json(res, 400, { error: 'text required' });
4544
+ const worker = applyReplyWorker(goal, requested);
4545
+ if (!worker.ok) return json(res, 409, { error: worker.error });
4527
4546
  // Reply picks a parked goal back up into the work queue. 'review' MUST be
4528
4547
  // here: a completed goal awaiting your approve/reject is 'review', and a
4529
4548
  // reply to it means "not done — keep working" (Masa 2026-07-24: replied to
@@ -4814,13 +4833,15 @@ async function answerGoalQuestion(goal, question) {
4814
4833
  let body = '';
4815
4834
  req.on('data', (d) => { body += d; });
4816
4835
  req.on('end', async () => {
4817
- let text = '', outcome = 'continue', auto = false;
4836
+ let text = '', outcome = 'continue', auto = false, requested = {};
4818
4837
  try {
4819
- const parsed = JSON.parse(body || '{}');
4820
- text = String(parsed.text ?? '').trim();
4821
- if (REPLY_OUTCOMES.includes(parsed.outcome)) outcome = parsed.outcome;
4822
- auto = parsed.outcome === 'auto';
4838
+ requested = JSON.parse(body || '{}');
4839
+ text = String(requested.text ?? '').trim();
4840
+ if (REPLY_OUTCOMES.includes(requested.outcome)) outcome = requested.outcome;
4841
+ auto = requested.outcome === 'auto';
4823
4842
  } catch {}
4843
+ const worker = applyReplyWorker(goal, requested);
4844
+ if (!worker.ok) return json(res, 409, { error: worker.error });
4824
4845
 
4825
4846
  // outcome:'auto' = 決めるのは AI 本人 (§1.4)。人の言葉を1回だけ渡して、返って
4826
4847
  // きた行き先のとおりに配線する。我々はキーワードも類似度も見ない。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@galda/cli",
3
- "version": "0.10.65",
3
+ "version": "0.10.67",
4
4
  "type": "module",
5
5
  "description": "Galda - hand off work to Claude Code or Codex, get proof back. Runs on your existing subscription, no extra API cost.",
6
6
  "scripts": {