@galda/cli 0.10.5 → 0.10.7

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
@@ -1239,15 +1239,16 @@
1239
1239
  #fsRoot .nowdoing .nd-pct{margin-left:auto;font:600 11px/1 var(--mono);color:var(--ink2);font-variant-numeric:tabular-nums;flex:0 0 auto}
1240
1240
  #fsRoot .nowdoing .nd-bar{flex:1 0 100%;height:2px;border-radius:2px;background:var(--hair);overflow:hidden}
1241
1241
  #fsRoot .nowdoing .nd-bar i{display:block;height:100%;border-radius:2px;background:linear-gradient(90deg,var(--edgeA,#C7B5FF),var(--edgeB,#7C9EFF))}
1242
- /* H19/§8-2: bottom-anchor the feed. .fsstream is the scroll viewport turned into a
1243
- flex column so #fsFeed's margin-top:auto pushes content downsparse/initial sits
1244
- just above the composer (no top-stuck limbo); once it overflows, margin-top:auto
1245
- collapses and it scrolls normally (latest at the bottom). Header (.chd/.nowdoing)
1246
- stays fixed above. */
1242
+ /* was H19/§8-2 bottom-anchor (#fsFeed{margin-top:auto}): sparse/initial content sat
1243
+ glued to the composer with a big blank gap above it read as "text appears from
1244
+ the bottom" (Masa bug report 2026-07-15, recurrence of an earlier fix). Feed now
1245
+ always starts at the top of .fsstream like a normal chat log; new lines still
1246
+ auto-follow to the bottom via the nearBottom check in renderFsFeed(). Header
1247
+ (.chd/.nowdoing) stays fixed above. */
1247
1248
  #fsRoot .fsstream{flex:1;min-height:0;overflow-y:auto;display:flex;flex-direction:column;max-width:var(--fscenterw,760px);width:100%;margin:0 auto;scrollbar-width:none;position:relative;z-index:1;
1248
1249
  transition:max-width .28s cubic-bezier(.4,0,.2,1)}
1249
1250
  #fsRoot .fsstream::-webkit-scrollbar{display:none}
1250
- #fsFeed{display:flex;flex-direction:column;gap:8px;padding-top:2px;margin-top:auto}
1251
+ #fsFeed{display:flex;flex-direction:column;gap:8px;padding-top:2px}
1251
1252
  #fsRoot .cc-say{font-size:13.5px;color:var(--ink)}
1252
1253
  /* Claude Code CLI language (HANDOFF-v45 §12, flagship .cc-t port): gap:0 — the bullet's
1253
1254
  own margin-right does the spacing, not a flex gap (keeps ⏺ tight against the tool name
@@ -2974,7 +2975,7 @@ function taskBlock(t){
2974
2975
  const lines = state.act[t.id] ?? [];
2975
2976
  body = `<div class="pbar"><i style="width:${progressFor(t)}%"></i></div><div class="pmeta">${progressMeta(t)}</div>
2976
2977
  ${todosHtml(t)}
2977
- <div class="tres runlog">${lines.map((l) => `<div class="actline">${esc(l)}</div>`).join('') || '<span class="actline">starting…</span>'}</div>`;
2978
+ <div class="tres runlog" data-runlog="${t.id}">${lines.map((l) => `<div class="actline">${esc(l)}</div>`).join('') || '<span class="actline">starting…</span>'}</div>`;
2978
2979
  } else if (t.status === 'done') {
2979
2980
  const files = t.changedFiles?.length ? `<div class="files"><b>changed:</b> ${t.changedFiles.map(esc).join(' · ')}</div>` : '';
2980
2981
  const usage = formatUsage(t.usage);
@@ -3117,6 +3118,15 @@ function renderStream(){
3117
3118
  // render never yanks the view while they read old history.
3118
3119
  const wasNearBottom = streamEl.scrollHeight - streamEl.clientHeight - streamEl.scrollTop <= 100;
3119
3120
  const keepScroll = streamEl.scrollTop;
3121
+ // Mirror of engine/lib.mjs nextRunlogScrollTop(): a running task's live log
3122
+ // (.tres.runlog) has its OWN scroll region and its HTML is rebuilt every SSE
3123
+ // render. Capture each one's geometry (keyed by task id) BEFORE the swap so we
3124
+ // can keep a user who scrolled up to re-read earlier lines pinned there instead
3125
+ // of yanking them to the bottom on every incoming activity line.
3126
+ const runlogPre = new Map();
3127
+ for (const el of streamEl.querySelectorAll('[data-runlog]')) {
3128
+ runlogPre.set(el.dataset.runlog, { scrollTop: el.scrollTop, scrollHeight: el.scrollHeight, clientHeight: el.clientHeight });
3129
+ }
3120
3130
  const items = mergeGoalTimeline(state.goals, state.externalActivity, state.active);
3121
3131
  // never-lose layer: memos not yet accepted by the server render from the
3122
3132
  // local outbox — visible, resendable, never silently gone
@@ -3168,7 +3178,10 @@ function renderStream(){
3168
3178
  inp.disabled = false;
3169
3179
  });
3170
3180
  }
3171
- for (const el of $('stream').querySelectorAll('.tres.runlog')) el.scrollTop = el.scrollHeight;
3181
+ for (const el of $('stream').querySelectorAll('[data-runlog]')) {
3182
+ const pre = runlogPre.get(el.dataset.runlog) ?? null;
3183
+ el.scrollTop = !pre || (pre.scrollHeight - pre.clientHeight - pre.scrollTop <= 100) ? el.scrollHeight : pre.scrollTop;
3184
+ }
3172
3185
  if (wasNearBottom) streamEl.scrollTop = streamEl.scrollHeight;
3173
3186
  else streamEl.scrollTop = keepScroll; // reading history: never yank the view
3174
3187
  }
@@ -10,9 +10,26 @@ import { spawnSync } from 'node:child_process';
10
10
  import { existsSync } from 'node:fs';
11
11
  import { join, resolve, dirname } from 'node:path';
12
12
  import { fileURLToPath, pathToFileURL } from 'node:url';
13
+ import { createServer } from 'node:net';
13
14
 
14
15
  const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
15
16
  const say = (m) => console.log(`[galda] ${m}`);
17
+
18
+ // Pick a free local port (starting at the requested one) so `npx @galda/cli`
19
+ // JUST WORKS even when the default 4400 is already taken — e.g. another manager
20
+ // is running on this machine. We do it HERE, before importing the server and
21
+ // spawning the relay-client, and export MANAGER_PORT, so both halves agree on
22
+ // the same port (the relay-client reads MANAGER_PORT too). No manual override.
23
+ const canBind = (p) => new Promise((res) => {
24
+ const s = createServer();
25
+ s.once('error', () => res(false));
26
+ s.once('listening', () => s.close(() => res(true)));
27
+ s.listen(p, '127.0.0.1');
28
+ });
29
+ async function pickPort(start) {
30
+ for (let p = start; p < start + 30; p++) { if (await canBind(p)) return p; }
31
+ return start; // give up → the server's own EADDRINUSE guard will explain
32
+ }
16
33
  // On Windows the `claude`/`ffmpeg` executables are .cmd/.ps1 shims that a bare
17
34
  // spawnSync can't resolve (ENOENT) — run through the shell there so PATHEXT
18
35
  // resolution kicks in. On macOS/Linux keep shell off (no injection surface).
@@ -69,6 +86,12 @@ process.env.MANAGER_OPEN_BROWSER = process.env.MANAGER_OPEN_BROWSER ?? '1';
69
86
  // and serves the whole API (/connect, /checkout, /api/*). See [[no-a2c-tech]].
70
87
  process.env.MANAGER_BILLING_API_URL = process.env.MANAGER_BILLING_API_URL ?? 'https://galda.app';
71
88
  process.env.RELAY_URL = process.env.RELAY_URL ?? 'wss://app.galda.app/agent';
89
+ // Choose a free port now and pin it for BOTH the server and the relay-client, so
90
+ // a busy 4400 (another manager) no longer stops onboarding — no MANAGER_PORT by hand.
91
+ const wantedPort = Number(process.env.MANAGER_PORT ?? 4400);
92
+ const port = await pickPort(wantedPort);
93
+ if (port !== wantedPort) say(`port ${wantedPort} is busy — using ${port} instead`);
94
+ process.env.MANAGER_PORT = String(port);
72
95
  await import(pathToFileURL(join(ROOT, 'engine', 'server.mjs')).href);
73
96
 
74
97
  // 5) fixed-URL relay (optional): if a relay is configured, connect this local
package/engine/lib.mjs CHANGED
@@ -674,6 +674,20 @@ export function nextStreamScrollTop(pre, newScrollHeight, threshold = 100) {
674
674
  : pre.scrollTop;
675
675
  }
676
676
 
677
+ // Same pin decision for the inner live worker-activity log (`.tres.runlog`) of a
678
+ // running task, whose HTML is fully rebuilt on every SSE render. `pre` is the
679
+ // element's geometry captured BEFORE the swap, or null if that runlog did not
680
+ // exist last render (a task that just started). A brand-new log pins to the
681
+ // bottom; an existing one follows the bottom only when the user was already near
682
+ // it — otherwise it keeps the exact prior scrollTop so scrolling up to re-read
683
+ // earlier lines mid-run is never yanked back down.
684
+ export function nextRunlogScrollTop(pre, newScrollHeight, threshold = 100) {
685
+ if (!pre) return newScrollHeight;
686
+ return shouldAutoScroll(pre.scrollTop, pre.scrollHeight, pre.clientHeight, threshold)
687
+ ? newScrollHeight
688
+ : pre.scrollTop;
689
+ }
690
+
677
691
  // GET /api/state payload for tasks (task 49): the browser's live activity
678
692
  // log (state.act) is otherwise only ever populated by streamed SSE 'act'
679
693
  // events, so a page refresh mid-run would blank a running task's log until
package/engine/server.mjs CHANGED
@@ -929,6 +929,7 @@ async function planGoal(goal) {
929
929
  const project = projects.find((p) => p.id === goal.projectId);
930
930
  const prompt = [
931
931
  'あなたはユーザーの依頼を受けるプロダクトマネージャ。以下のユーザー依頼を、ユーザーから見た「成果物(トピック)」の単位で整理する。エンジニアリングの工程には分解しない。',
932
+ '【言語】ユーザーが読む文字列(各タスクの title、確認の question / options)は、末尾の「依頼:」と同じ言語で書く。英語の依頼には英語で、日本語の依頼には日本語で返す(例: "test" → 英語、"テスト" → 日本語)。detail(worker への指示)は言語自由。',
932
933
  '- 既定は 1トピック=1タスク。1つのまとまった依頼を実装工程に割らない(「PRDを作る」「パーサを追加」「テストを書く」「リファクタ」等を別タスクにしない)。それらは1人のworkerが内部で全部やる(実装→テスト→proof/検証まで一気通貫)。',
933
934
  '- 複数タスクに分けるのは、依頼に「明確に別々の成果物・トピック」が含まれる時「だけ」(例:「音声入力を追加して、あとビジュアル作成も」→ 音声入力 / ビジュアル作成 の2つ)。工程での分割は絶対にしない。',
934
935
  '- 被っている・言い換えているだけの依頼は、自分で理解して1つにまとめる。',
@@ -940,7 +941,7 @@ async function planGoal(goal) {
940
941
  // Once the user has answered a clarification, NEVER ask again — re-asking looped
941
942
  // the answered question back onto the board. Override the rule above for re-plans.
942
943
  goal.clarified ? '【最重要】ユーザーは既にこの依頼への確認質問に回答済み(依頼文末尾に [確認: … → …] として反映済み)。これ以上、確認質問(question)を返してはならない。回答を前提に、必ずタスクへ分解して返すこと。' : '',
943
- '- 出力はJSONのみ(前後に文章を書かない)。通常: {"entry":"path|null","tasks":[{"title":"ユーザーから見た成果(短い日本語)","detail":"workerへの完全な指示","passCondition":"検証条件|null"}]} / 確認が要る時だけ: {"question":"...","options":["...","..."]}',
944
+ '- 出力はJSONのみ(前後に文章を書かない)。通常: {"entry":"path|null","tasks":[{"title":"ユーザーから見た成果(依頼と同じ言語で短く)","detail":"workerへの完全な指示","passCondition":"検証条件|null"}]} / 確認が要る時だけ: {"question":"...(依頼と同じ言語)","options":["...","..."]}',
944
945
  '',
945
946
  goal.priorFailureMemory?.length ? [
946
947
  '過去の失敗メモリ(同じ失敗を避けること):',
@@ -1312,6 +1313,9 @@ function testOutputExcerpt(text) {
1312
1313
  }
1313
1314
 
1314
1315
  function workerPrompt(task, goal, lastFailure) {
1316
+ // Report back to the human in the SAME language they wrote the goal in (Masa
1317
+ // 2026-07-15: "撃った言語で返す"). JP characters → Japanese, otherwise English.
1318
+ const replyLang = /[぀-ヿ㐀-鿿]/.test(goal?.text || task?.title || '') ? '日本語' : 'English';
1315
1319
  // Optional skill the user attached to this goal. Empirically (scratch worker,
1316
1320
  // haiku, custom fixture skill) the reliably-firing form is an explicit Skill-tool
1317
1321
  // instruction as the FIRST line; a bare "/name" first line did not consistently
@@ -1343,7 +1347,7 @@ function workerPrompt(task, goal, lastFailure) {
1343
1347
  '- 変更に対応するテストを書く。確認は該当テストファイル1つだけを1回実行する(例: node --test path/to/only-this.test.mjs)。プロジェクト全体の npm test を繰り返し回さない — 全体テストと最終検証は Manager 側が実行する。テストが適用できない変更は理由と手動確認手順を書く。',
1344
1348
  '- 検証しすぎない: 自分の変更が動くと確認できたら、それ以上ログを漁ったり再検証を重ねたりせず、すぐに報告して終える(時間と token の無駄を避ける)。',
1345
1349
  '- Do not commit (git is handled by the Manager).',
1346
- '- 最後に日本語で2〜4行:何を変えたか・どのファイルか・テスト結果・人間が確認すべき点。',
1350
+ `- 最後に${replyLang}で2〜4行:何を変えたか・どのファイルか・テスト結果・人間が確認すべき点。`,
1347
1351
  ].filter(Boolean).join('\n');
1348
1352
  }
1349
1353
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@galda/cli",
3
- "version": "0.10.5",
3
+ "version": "0.10.7",
4
4
  "type": "module",
5
5
  "description": "Galda - hand off work to your Claude Code, get proof back. Runs on your existing subscription, no extra API cost.",
6
6
  "scripts": {