@galda/cli 0.10.64 → 0.10.66

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
@@ -3373,7 +3373,7 @@ const savedTheme = (() => {
3373
3373
  if (v !== s) localStorage.setItem('theme', v);
3374
3374
  return v;
3375
3375
  })();
3376
- const state = { repos: null, repoHome: '', /* GET /api/repos — the folders a project can point at (Context Bar) */ projects: [], goals: [], tasks: [], act: {}, actAt: {}, todos: {}, chatTurns: {}, active: localStorage.getItem('sel:project') || 'default', editing: null, attach: [], connectCommand: '', models: ['sonnet'], agentModels: null, agentEfforts: null, agents: ['claude-code', 'codex'], expanded: new Set(), openThreads: new Set(), queueOrder: {}, externalActivity: [], selectedGoal: null, workflowColumns: {}, wfDraft: null, reviewDefinitions: {}, rdDraft: null, connectedApp: localStorage.getItem('sel:agent') || 'claude-code', reviewOpenGoal: null, reviewApprovals: {}, goalDetailOpen: null, qreplyOpen: new Set(), logCollapsed: localStorage.getItem('logCollapsed') !== '0', logEntries: [], layout: localStorage.getItem('layout') || 'flagship', theme: savedTheme, boardskin: localStorage.getItem('boardskin') || 'auto', summaryPattern: Number(localStorage.getItem('summaryPattern')) || 3, peraSpot: 0, skill: null, skillsCache: null, projectRules: {}, rulesOpen: false,
3376
+ const state = { repos: null, repoHome: '', /* GET /api/repos — the folders a project can point at (Context Bar) */ projects: [], goals: [], tasks: [], act: {}, actAt: {}, todos: {}, chatTurns: {}, active: localStorage.getItem('sel:project') || 'default', editing: null, attach: [], connectCommand: '', models: ['sonnet'], agentModels: null, agentEfforts: null, agents: ['claude-code', 'codex'], auth: null, expanded: new Set(), openThreads: new Set(), queueOrder: {}, externalActivity: [], selectedGoal: null, workflowColumns: {}, wfDraft: null, reviewDefinitions: {}, rdDraft: null, connectedApp: localStorage.getItem('sel:agent') || 'claude-code', reviewOpenGoal: null, reviewApprovals: {}, goalDetailOpen: null, qreplyOpen: new Set(), logCollapsed: localStorage.getItem('logCollapsed') !== '0', logEntries: [], layout: localStorage.getItem('layout') || 'flagship', theme: savedTheme, boardskin: localStorage.getItem('boardskin') || 'auto', summaryPattern: Number(localStorage.getItem('summaryPattern')) || 3, peraSpot: 0, skill: null, skillsCache: null, projectRules: {}, rulesOpen: false,
3377
3377
  // per-message send overrides set via slash-commands (reset after each send,
3378
3378
  // except model which is sticky in localStorage). palette = open command list.
3379
3379
  msg: { mode: null, model: null, effort: null, skill: null, later: false, review: false },
@@ -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();
@@ -7725,10 +7728,11 @@ const AUTH_COPY = {
7725
7728
  };
7726
7729
  function renderAuthBanner(auth){
7727
7730
  const bar = $('errbar');
7728
- if (auth && auth.ok === false){
7731
+ state.auth = auth ?? state.auth;
7732
+ if (state.connectedApp === 'claude-code' && state.auth?.ok === false){
7729
7733
  authDown = true;
7730
7734
  const copy = AUTH_COPY[billingLang()];
7731
- const limited = auth.reason === 'rate-limited';
7735
+ const limited = state.auth.reason === 'rate-limited';
7732
7736
  const canUseCodex = (state.agents || []).includes('codex');
7733
7737
  bar.classList.add('billing'); // strips #errbar's error fill so the card stands alone
7734
7738
  bar.innerHTML = `<div class="bx-ask${limited ? ' info' : ''}"><div class="bx-qt"><div class="bx-lb">${limited ? copy.limitTitle : copy.connectTitle}</div>`
@@ -8024,7 +8028,7 @@ async function refresh(){
8024
8028
  state.connectCommand = s.connectCommand ?? ''; state.models = s.models ?? ['sonnet']; state.agentModels = s.agentModels ?? null; state.agentEfforts = s.agentEfforts ?? null; state.agents = s.agents ?? ['claude-code', 'codex']; state.uiVersion = s.uiVersion;
8025
8029
  state.billing = s.billing ?? { licensed: false, email: null };
8026
8030
  renderAppSelect(); // picker follows the server's installed-agent set; may correct a stale connectedApp
8027
- renderAuthBanner(s.auth); // show/clear the worker sign-in banner from the boot preflight
8031
+ renderAuthBanner(s.auth); // only the selected worker's auth can raise a banner
8028
8032
  setAgentModel(state.connectedApp);
8029
8033
  setAgentEffort(state.connectedApp);
8030
8034
  renderModelSelect();
@@ -8121,7 +8125,7 @@ function connectSSE(){
8121
8125
  if (!state.projects.some((p) => p.id === state.active)) state.active = state.projects[0]?.id;
8122
8126
  }
8123
8127
  else if (m.ev === 'ping') { checkUiVersion(m.uiVersion); return; }
8124
- else if (m.ev === 'auth') { renderAuthBanner(m.auth); return; } // worker sign-in preflight result
8128
+ else if (m.ev === 'auth') { renderAuthBanner(m.auth); return; } // selected worker's sign-in preflight result
8125
8129
  scheduleRender();
8126
8130
  };
8127
8131
  es.onerror = () => {
@@ -8576,7 +8580,7 @@ async function send(){
8576
8580
  } else {
8577
8581
  // running/partial/failed/interrupted/blocked → a plain follow-up on the same goal.
8578
8582
  const r = await fetch(withKey(`/api/goals/${bind.goal.id}/reply`), { method: 'POST',
8579
- headers: { 'content-type': 'application/json' }, body: JSON.stringify({ text: fullText }) });
8583
+ headers: { 'content-type': 'application/json' }, body: JSON.stringify(replyWorkerPayload({ text: fullText })) });
8580
8584
  if (!r.ok) throw new Error();
8581
8585
  const g = await r.json(); const goal = g.goal ?? g;
8582
8586
  if (goal?.id) upsert(state.goals, goal);
@@ -9780,7 +9784,7 @@ async function saSendReply(it, i, text){
9780
9784
  it.st = 3; it.rstatus = 'working'; SA.cur = goalId;
9781
9785
  saPaintCard(goalId);
9782
9786
  try {
9783
- 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 })) });
9784
9788
  if (!r.ok) throw new Error('reply failed');
9785
9789
  if (isAttn) {
9786
9790
  // A Needs-you reply picks the goal back up → it belongs in Doing, not lingering in
@@ -10213,6 +10217,7 @@ $('appmenu').addEventListener('click', (e) => {
10213
10217
  renderModelSelect();
10214
10218
  renderEffortSelect();
10215
10219
  renderMsgHint();
10220
+ renderAuthBanner(state.auth);
10216
10221
  $('appmenu').classList.remove('show');
10217
10222
  const projectId = state.active;
10218
10223
  if (projectId) fetch(withKey(`/api/projects/${encodeURIComponent(projectId)}/queue-agent`), {
package/engine/server.mjs CHANGED
@@ -1973,6 +1973,19 @@ function resumeProject(projectId) {
1973
1973
 
1974
1974
  // Slack風スレッド返信タスクの生成(/api/goals/:id/reply と /api/goals/:id/dismiss で共有)。
1975
1975
  // runTask()のtask.reply分岐がgoal.sessionIdをresumeに渡して同じ会話を続ける。
1976
+ function applyReplyWorker(goal, requested = {}) {
1977
+ if (requested.agent == null) return { ok: true };
1978
+ if (!WORKER_AGENTS.includes(requested.agent)) return { ok: false, error: 'unsupported agent' };
1979
+ const agent = workerAgent(requested.agent);
1980
+ if (!AVAILABLE_AGENTS.includes(agent)) return { ok: false, error: `${agent} is not connected on this device` };
1981
+ const switchedAgent = agent !== goal.agent;
1982
+ goal.agent = agent;
1983
+ goal.model = workerModel(agent, requested.model);
1984
+ goal.effort = workerEffort(agent, requested.effort);
1985
+ if (switchedAgent) goal.sessionId = null;
1986
+ return { ok: true };
1987
+ }
1988
+
1976
1989
  function queueReplyTask(goal, text, { from = 'manager', via = 'composer' } = {}) {
1977
1990
  // Every follow-up on a goal is part of its conversation (§1.1). Recorded here
1978
1991
  // rather than at each call site so an internally-generated nudge (auto-retest
@@ -4522,8 +4535,11 @@ const server = createServer(async (req, res) => {
4522
4535
  req.on('data', (d) => { body += d; });
4523
4536
  req.on('end', () => {
4524
4537
  try {
4525
- const { text } = JSON.parse(body);
4538
+ const requested = JSON.parse(body);
4539
+ const { text } = requested;
4526
4540
  if (!text?.trim()) return json(res, 400, { error: 'text required' });
4541
+ const worker = applyReplyWorker(goal, requested);
4542
+ if (!worker.ok) return json(res, 409, { error: worker.error });
4527
4543
  // Reply picks a parked goal back up into the work queue. 'review' MUST be
4528
4544
  // here: a completed goal awaiting your approve/reject is 'review', and a
4529
4545
  // reply to it means "not done — keep working" (Masa 2026-07-24: replied to
@@ -4814,13 +4830,15 @@ async function answerGoalQuestion(goal, question) {
4814
4830
  let body = '';
4815
4831
  req.on('data', (d) => { body += d; });
4816
4832
  req.on('end', async () => {
4817
- let text = '', outcome = 'continue', auto = false;
4833
+ let text = '', outcome = 'continue', auto = false, requested = {};
4818
4834
  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';
4835
+ requested = JSON.parse(body || '{}');
4836
+ text = String(requested.text ?? '').trim();
4837
+ if (REPLY_OUTCOMES.includes(requested.outcome)) outcome = requested.outcome;
4838
+ auto = requested.outcome === 'auto';
4823
4839
  } catch {}
4840
+ const worker = applyReplyWorker(goal, requested);
4841
+ if (!worker.ok) return json(res, 409, { error: worker.error });
4824
4842
 
4825
4843
  // outcome:'auto' = 決めるのは AI 本人 (§1.4)。人の言葉を1回だけ渡して、返って
4826
4844
  // きた行き先のとおりに配線する。我々はキーワードも類似度も見ない。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@galda/cli",
3
- "version": "0.10.64",
3
+ "version": "0.10.66",
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": {