@galda/cli 0.10.118 → 0.10.120

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
@@ -328,6 +328,11 @@
328
328
  hairline-separated from the detected list, matching .appopt grammar. */
329
329
  .ctxmenu .appopt.ctxopen{flex-direction:row;align-items:center;gap:7px;border-top:1px solid var(--hair);margin-top:4px;padding-top:8px}
330
330
  .ctxmenu .appopt.ctxopen svg{width:14px;height:14px;flex:0 0 14px;stroke:currentColor;stroke-width:1.7;fill:none;stroke-linecap:round;stroke-linejoin:round}
331
+ /* Working: the folder icon becomes a spinner in its own place, so the row
332
+ itself says a chooser is opening rather than only the line of text below it.
333
+ Reuses @keyframes spin — no new motion vocabulary (DESIGN-RULES §5.5). */
334
+ .ctxmenu .appopt.ctxopen .ctxspin{width:12px;height:12px;flex:0 0 14px;margin:1px;border-radius:50%;
335
+ border:1.5px solid var(--ink3);border-top-color:transparent;animation:spin 1s linear infinite}
331
336
  .ctxmenu .appopt.ctxopen .nm{color:var(--ink);font-size:12.5px;line-height:1.3}
332
337
  .appopt{display:flex;align-items:center;gap:7px;height:28px;padding:0 8px;border:0;border-radius:6px;background:transparent;
333
338
  color:var(--ink2);font:inherit;font-size:12px;cursor:pointer;text-align:left;width:100%}
@@ -529,15 +534,18 @@
529
534
  .badge.upnext{color:var(--invink);background:rgba(var(--tint),.75);border:0;font-weight:700}
530
535
  .retrybtn{margin-left:6px;padding:1.5px 9px;border:1px solid var(--hair2);border-radius:6px;background:transparent;color:var(--ink);font-size:10.5px;cursor:pointer}
531
536
  .retrybtn.skip{color:var(--ink3)} /* Dismiss reads as the secondary/clear action next to Retry */
532
- /* Queue priority badge (To do rows only) — click opens a 高/中/低 popup.
533
- No new hues: borrows the existing --amber attention accent, 中/低 stay
534
- on the neutral ink scale, so priority never reads as another rainbow. */
537
+ /* Queue priority badge (To do rows only) — click opens a High/Medium/Low popup.
538
+ No new hues: high borrows the existing --amber attention accent, medium/low
539
+ stay on the neutral ink scale, so priority never reads as another rainbow.
540
+ One mono glyph in a 22px square, unchanged since it held 高: the square is
541
+ the shape, H/M/L is what fits in it, and the whole word is in the menu where
542
+ there is room for it. */
535
543
  .priosel{position:relative;flex:0 0 auto;align-self:flex-start;margin-top:1px}
536
544
  .priobtn{width:22px;height:20px;border:1px solid var(--hair2);border-radius:6px;background:transparent;
537
545
  color:var(--ink3);font-family:var(--mono);font-size:10.5px;cursor:pointer;display:grid;place-items:center}
538
546
  .priobtn:hover{color:var(--ink);border-color:rgba(var(--tint),.2)}
539
- .priobtn.prio-高{color:var(--amber);border-color:rgba(var(--amber-rgb),.4)}
540
- .priobtn.prio-中{color:var(--ink2)}
547
+ .priobtn.prio-high{color:var(--amber);border-color:rgba(var(--amber-rgb),.4)}
548
+ .priobtn.prio-medium{color:var(--ink2)}
541
549
  .priomenu{position:absolute;top:calc(100% + 4px);right:0;min-width:64px;background:var(--surface);border:1px solid var(--hair2);
542
550
  border-radius:9px;padding:4px;display:none;flex-direction:column;gap:1px;box-shadow:0 10px 28px rgba(0,0,0,.5);z-index:50}
543
551
  .priomenu.show{display:flex}
@@ -666,6 +674,18 @@
666
674
  .peratab.on{color:var(--ink);border-color:var(--ink3);background:rgba(var(--tint),.06)}
667
675
  .peraask{display:flex;gap:8px;align-items:center;padding:10px 16px 6px}
668
676
  .peraask input{flex:1;background:var(--glass);border:1px solid var(--hair2);border-radius:10px;color:var(--ink);font-size:12.5px;padding:8px 12px;outline:none}
677
+ /* First-run nudge + the npx answer. Deliberately not a painted panel or a
678
+ boxed card: a box reads as "tappable" (DESIGN-RULES §5.6). This is a line
679
+ of text and a stroke pointing into the input it is about. */
680
+ .cnudge{display:none;align-items:center;justify-content:center;gap:10px;margin:14px auto 0;opacity:0;transform:translateY(-3px);
681
+ transition:opacity .28s ease,transform .28s ease}
682
+ .cnudge.show{display:flex;opacity:1;transform:none}
683
+ .cnudge-t{font-size:12.5px;line-height:1.5;color:var(--ink2)}
684
+ .cnudge-a{width:12px;height:20px;flex:0 0 auto;border-left:1.5px solid var(--ink3);border-bottom:1.5px solid var(--ink3);
685
+ border-bottom-left-radius:7px;position:relative;opacity:.85}
686
+ .cnudge-a:after{content:'';position:absolute;right:-1px;bottom:-4px;width:6px;height:6px;
687
+ border-right:1.5px solid var(--ink3);border-bottom:1.5px solid var(--ink3);transform:rotate(-45deg)}
688
+ padding:10px 12px;font-family:var(--mono);font-size:12.5px;color:var(--ink);overflow-x:auto;white-space:nowrap}
669
689
  .peraanswer{margin:0 16px 8px;font-size:12.5px;line-height:1.55;color:var(--ink2);white-space:pre-wrap;background:rgba(var(--tint),.03);border:1px solid var(--hair2);border-radius:10px;padding:10px 12px}
670
690
  .peraanswer.loading{color:var(--ink3);font-style:italic}
671
691
  .peraanswer.error{color:var(--danger)}
@@ -2923,13 +2943,31 @@
2923
2943
  /* ---- iOS no-zoom: 16px inputs (extends the shared 16px rule to flagship-only fields) ---- */
2924
2944
  body[data-layout="flagship"] .sa-chat,
2925
2945
  body[data-layout="flagship"] #fsAskbar input{font-size:16px}
2926
- /* ---- Ledger (#seeall) — full-bleed sheet + no-clip action row ---- */
2946
+ /* ---- Ledger (#seeall) — Claude Code-style mobile reading sheet.
2947
+ A tapped Review should land at the top of the phone viewport, with the
2948
+ content column using the width instead of spending it on desktop gutters. ---- */
2949
+ body[data-layout="flagship"] #seeall.open{place-items:start center;z-index:120;pointer-events:auto}
2927
2950
  body[data-layout="flagship"] #seeall .sa-panel{width:100%!important;max-width:100%!important;
2928
- margin-top:0;max-height:100%;border-radius:0}
2929
- body[data-layout="flagship"] #seeall .sa-hd{padding:14px 16px 10px}
2951
+ margin-top:calc(env(safe-area-inset-top) + 6px)!important;
2952
+ max-height:calc(100dvh - env(safe-area-inset-top) - env(safe-area-inset-bottom) - 12px)!important;
2953
+ border-radius:18px 18px 0 0!important}
2954
+ body[data-layout="flagship"] #seeall .sa-hd{padding:14px 18px 10px!important}
2930
2955
  body[data-layout="flagship"] #seeall .sa-hd .keys{display:none} /* keyboard shortcuts — no keyboard on a phone */
2931
- body[data-layout="flagship"] #seeall .sa-body{padding:12px 14px 16px}
2956
+ body[data-layout="flagship"] #seeall .sa-body{padding:10px 8px 16px}
2932
2957
  body[data-layout="flagship"] #seeall .sa-item{width:100%}
2958
+ body[data-layout="flagship"] #seeall .sa-item.redesign{padding:22px 18px 28px!important}
2959
+ body[data-layout="flagship"] #seeall .sa-body>.sa-item.redesign+.sa-item.redesign{padding-top:30px!important}
2960
+ body[data-layout="flagship"] #seeall .sa-panel .sa-hd:has(~ .sa-body .sa-item.redesign){padding:14px 18px 10px!important}
2961
+ body[data-layout="flagship"] #seeall .revrail.show{width:9px;margin-right:7px}
2962
+ body[data-layout="flagship"] #seeall .revrail button{font-size:0;padding:6px 0}
2963
+ body[data-layout="flagship"] #seeall .revrail button .bar{width:3px;height:18px}
2964
+ body[data-layout="flagship"] #seeall .revrail button.on .bar{height:28px}
2965
+ body[data-layout="flagship"] #seeall .redesign .rtitle{font-size:17px;line-height:1.32;letter-spacing:0}
2966
+ body[data-layout="flagship"] #seeall .redesign .rsec{margin-top:22px!important}
2967
+ body[data-layout="flagship"] #seeall .redesign .rsec-h{font-size:15px;letter-spacing:0}
2968
+ body[data-layout="flagship"] #seeall .redesign .rbody,
2969
+ body[data-layout="flagship"] #seeall .redesign .rreport{font-size:15px;line-height:1.72;letter-spacing:0;max-width:none}
2970
+ body[data-layout="flagship"] #seeall .redesign .rreport code{font-size:13px}
2933
2971
  body[data-layout="flagship"] #seeall .sa-act{flex-wrap:wrap;row-gap:8px}
2934
2972
  body[data-layout="flagship"] #seeall .sa-chatwrap{flex:1 1 100%;order:3;min-width:0}
2935
2973
  body[data-layout="flagship"] #seeall .sa-app,
@@ -2963,8 +3001,15 @@
2963
3001
  max-width:62%;justify-content:center;margin:0}
2964
3002
  /* Review/To Do opener (right) = plain ☰ icon (Masa 2026-07-18: ボタン化はやめ、三本線だけ・
2965
3003
  タップで To Do/Review ドロワー). The base .fshamb rule above already renders the ☰ svg. */
2966
- body[data-layout="flagship"] #fsRoot .fshamb .fshamb-lb,
2967
- body[data-layout="flagship"] #fsRoot .fshamb .fshamb-n{display:none}
3004
+ body[data-layout="flagship"] #fsRoot .fshamb .fshamb-lb{display:none}
3005
+ /* Masa 2026-07-31: たまったレビューが右上から分からない ☰ の右肩に赤ドット。
3006
+ 件数はJS(renderFsReview)がfshamb-nに書き込み済みだったが display:none で常に非表示だった。
3007
+ 件数の有無だけをJSからのtext contentで判定して出し分ける(空なら消える)。 */
3008
+ body[data-layout="flagship"] #fsRoot .fshamb{position:relative}
3009
+ body[data-layout="flagship"] #fsRoot .fshamb .fshamb-n{display:none;position:absolute;top:2px;right:2px;
3010
+ min-width:9px;height:9px;padding:0;border-radius:50%;background:var(--danger);
3011
+ box-shadow:0 0 0 2px var(--panel);font-size:0;line-height:0;color:transparent}
3012
+ body[data-layout="flagship"] #fsRoot .fshamb .fshamb-n:not(:empty){display:block}
2968
3013
  /* Top bar = chromeless on mobile for ALL themes (Masa 2026-07-18: Midnight で上に変なフレーム
2969
3014
  が出ていた → Gradient と同じく枠/面なしに). Gradient already did this; extend to every theme. */
2970
3015
  body[data-layout="flagship"] #fsRoot .fsrail{background:transparent;box-shadow:none}
@@ -2984,9 +3029,10 @@
2984
3029
  margin:0 0 4px;padding:0 2px 10px;gap:9px;border-bottom:1px solid var(--hair)}
2985
3030
  /* H25③: See-all ledger → scrim + inset panel (a full-bleed sheet let translucent themes show through). */
2986
3031
  body[data-layout="flagship"] #seeall{background:rgba(9,12,20,.55)}
3032
+ body[data-layout="flagship"] #seeall.open{z-index:120}
2987
3033
  body[data-layout="flagship"] #seeall .sa-panel{width:96%!important;max-width:96%!important;
2988
3034
  max-height:calc(100dvh - env(safe-area-inset-top) - 22px)!important;
2989
- margin-top:calc(env(safe-area-inset-top) + 10px)!important;border-radius:16px!important}
3035
+ margin-top:calc(env(safe-area-inset-top) + 6px)!important;border-radius:18px!important}
2990
3036
  }
2991
3037
  /* Billing UI (H16/H22) — self-serve plan. Tokens only; red is a line/label,
2992
3038
  never a fill (DESIGN-RULES §5.6), and it is --danger (the app's real token —
@@ -3249,6 +3295,13 @@
3249
3295
  <div class="nowdoing" id="fsNowDoing" hidden></div>
3250
3296
  <!-- Clawd HERO (§4.5 初期状態): filled by JS (CLAWD generator), shown via #fsRoot.clawdhero -->
3251
3297
  <div id="fsClawdHero" aria-hidden="true"></div>
3298
+ <!-- Under the waiting hero, pointing down at the box a new person is meant
3299
+ to use. "CONNECTED / WAITING FOR GOALS" says our state; this says
3300
+ theirs. Shown only while the board has nothing on it. -->
3301
+ <div class="cnudge" id="composerNudge">
3302
+ <span class="cnudge-t">Type what you want done, down there.</span>
3303
+ <span class="cnudge-a" aria-hidden="true"></span>
3304
+ </div>
3252
3305
  <!-- H23 mobile empty-state hero (flagship .m-hero) — chips fill the composer (#input) -->
3253
3306
  <div class="m-hero"><h2>What should we build?</h2><p>Describe a goal in plain language. Claude Code runs it, checks its own work, and only calls it done with proof.</p><div class="m-chips"><button class="m-chip">Fix the failing test in lib.mjs</button><button class="m-chip">Add a token-usage counter</button><button class="m-chip">Summarize the open PRs</button></div></div>
3254
3307
  <div class="fsstream"><div id="fsFeed"></div></div>
@@ -3440,6 +3493,20 @@
3440
3493
  <div class="segbtn" id="langSeg"><button data-lang="auto">Auto</button><button data-lang="en">English</button><button data-lang="ja">日本語</button></div></div>
3441
3494
  </div>
3442
3495
  </div></div>
3496
+ <!-- "What is Galda? / What can it access?" — the orientation panel.
3497
+ WHY IT EXISTS: the first engineer we handed Galda to reached for the gear
3498
+ button before typing anything. He was not lost, he was CHECKING: people
3499
+ want to know what a tool can touch before they hand it a repository. This
3500
+ answers that where they already are, instead of sending them to a website.
3501
+ Rendered from a constant, entirely locally: NO agent run, NO tokens, no
3502
+ network. Spending someone's quota and 40 seconds to produce a text answer
3503
+ they could have read instantly would be a bad trade, and it would teach
3504
+ the wrong idea of what Galda is (it is not a chatbot).
3505
+ Same overlay/modal parts as Settings above — no new component. -->
3506
+ <div class="overlay revfloat" id="aboutOverlay"><div class="modal" style="width:min(560px,94vw)">
3507
+ <div class="perahead" style="border-bottom:1px solid var(--hair)"><div class="peratitle" id="aboutTitle">What is Galda?</div><span style="flex:1"></span><button class="mbtn ghost" id="aboutClose">Close</button></div>
3508
+ <div style="padding:16px;max-height:min(70vh,620px);overflow:auto" id="aboutBody"></div>
3509
+ </div></div>
3443
3510
  <!-- Slack-style centered delete confirmation, replacing native confirm() for every goal
3444
3511
  delete (restores a lost decision — see docs/confirmed-behaviors.md). Reusable via the
3445
3512
  confirmDelete({title,preview}) -> Promise<boolean> helper. -->
@@ -3710,18 +3777,34 @@ function pickColumnLabel(columns, bucket, fallback){
3710
3777
  }
3711
3778
  function columnsFor(projectId){ return state.workflowColumns[projectId] ?? DEFAULT_WORKFLOW_COLUMNS; }
3712
3779
 
3713
- // Mirrors engine/lib.mjs TASK_PRIORITIES — kept in sync manually, see the
3714
- // comment on DEFAULT_WORKFLOW_COLUMNS above. Queue execution order is
3715
- // 高→中→低, same-priority ties keep creation/drag order (server.mjs's
3716
- // sortQueueByPriority) — this badge is only for To do (queued) rows, where
3717
- // that ordering actually matters.
3718
- const TASK_PRIORITIES = ['高', '中', '低'];
3780
+ // Mirrors engine/lib.mjs TASK_PRIORITIES / normalizePriority — kept in sync
3781
+ // manually, see the comment on DEFAULT_WORKFLOW_COLUMNS above. Queue execution
3782
+ // order is high→medium→low, same-priority ties keep creation/drag order
3783
+ // (server.mjs's sortQueueByPriority) — this badge is only for To do (queued)
3784
+ // rows, where that ordering actually matters.
3785
+ //
3786
+ // The values were 高/中/低 until 2026-07-31. Nothing migrates what is already on
3787
+ // disk, so every task written before then still says 高 and must still render as
3788
+ // what it is: reading only the new list would have shown it as Medium — the board
3789
+ // lying about the order the queue really runs in.
3790
+ const TASK_PRIORITIES = ['high', 'medium', 'low'];
3791
+ const LEGACY_PRIORITIES = { 高: 'high', 中: 'medium', 低: 'low' };
3792
+ // One glyph for the 22px square, the whole word for the menu that has room.
3793
+ const PRIORITY_GLYPH = { high: 'H', medium: 'M', low: 'L' };
3794
+ const PRIORITY_NAME = { high: 'High', medium: 'Medium', low: 'Low' };
3795
+ function normalizePriority(value){
3796
+ const v = String(value ?? '').trim();
3797
+ if (TASK_PRIORITIES.includes(v)) return v;
3798
+ if (LEGACY_PRIORITIES[v]) return LEGACY_PRIORITIES[v];
3799
+ const lower = v.toLowerCase();
3800
+ return TASK_PRIORITIES.includes(lower) ? lower : 'medium';
3801
+ }
3719
3802
  function prioBadge(t){
3720
- const p = TASK_PRIORITIES.includes(t.priority) ? t.priority : '中';
3803
+ const p = normalizePriority(t.priority);
3721
3804
  return `<div class="priosel" data-priosel="${t.id}">
3722
- <button type="button" class="priobtn prio-${p}" data-priobtn="${t.id}" title="Change priority">${p}</button>
3805
+ <button type="button" class="priobtn prio-${p}" data-priobtn="${t.id}" title="Change priority — ${PRIORITY_NAME[p]}">${PRIORITY_GLYPH[p]}</button>
3723
3806
  <div class="priomenu" data-priomenu="${t.id}">${TASK_PRIORITIES.map((x) =>
3724
- `<button type="button" class="prioopt${x === p ? ' active' : ''}" data-priotask="${t.id}" data-val="${x}">${x}</button>`).join('')}</div>
3807
+ `<button type="button" class="prioopt${x === p ? ' active' : ''}" data-priotask="${t.id}" data-val="${x}">${PRIORITY_NAME[x]}</button>`).join('')}</div>
3725
3808
  </div>`;
3726
3809
  }
3727
3810
 
@@ -6732,6 +6815,7 @@ $('gdRevisionInput').addEventListener('keydown', (e) => {
6732
6815
  });
6733
6816
 
6734
6817
  function render(){
6818
+ renderComposerNudge();
6735
6819
  renderProjects(); renderStream(); renderQueue(); renderTasks(); renderActPanel(); wireWfEditButtons();
6736
6820
  renderCtxBar(); // the folder above the composer follows the active project
6737
6821
  renderFlagship();
@@ -9018,6 +9102,12 @@ function connectSSE(){
9018
9102
  // fsActiveContext() uses to pick which running goal is "now" when several run
9019
9103
  // in parallel (§1.5-1: never anchor on stale work when something is live).
9020
9104
  else if (m.ev === 'act') { (state.act[m.taskId] ??= []).push(m.line); state.act[m.taskId] = state.act[m.taskId].slice(-1000); state.actAt[m.taskId] = Date.now(); scheduleActRender(); return; }
9105
+ // The agent talking, as opposed to the agent working. Everything above this
9106
+ // line is machinery (a status, a tool call, a to-do list); this is a sentence
9107
+ // somebody wrote to the person waiting, so it goes where the person is
9108
+ // looking — the same turn treatment a chat reply gets, because it is the same
9109
+ // thing. Masa's friend waited three minutes for exactly this and gave up.
9110
+ else if (m.ev === 'say') { sayFromAgent(m); return; }
9021
9111
  else if (m.ev === 'todos') state.todos[m.taskId] = m.todos ?? [];
9022
9112
  else if (m.ev === 'columns') state.workflowColumns[m.projectId] = m.columns;
9023
9113
  else if (m.ev === 'review-definition') state.reviewDefinitions[m.projectId] = m.reviewDefinition;
@@ -9071,7 +9161,8 @@ function bootDisconnected(info){
9071
9161
  background:rgba(4,4,6,.72);-webkit-backdrop-filter:blur(7px);backdrop-filter:blur(7px)}
9072
9162
  #connectGate.on{display:flex}
9073
9163
  #connectGate .cg-card{position:relative;width:100%;max-width:440px;background:var(--panel,#0d0d0f);border:1px solid var(--hair2,rgba(255,255,255,.12));
9074
- border-radius:18px;padding:30px 28px;text-align:center;box-shadow:0 30px 80px -30px #000}
9164
+ border-radius:18px;padding:28px 26px;text-align:left;box-shadow:0 30px 80px -30px #000;
9165
+ max-height:calc(100vh - 44px);overflow:auto}
9075
9166
  #connectGate h2{font:700 20px/1.3 var(--ui,system-ui);letter-spacing:-.02em;margin:0 0 8px;color:var(--ink,#e9e7e2)}
9076
9167
  #connectGate p{font-size:13.5px;line-height:1.7;color:var(--ink2,#a3a097);margin:0 0 18px}
9077
9168
  #connectGate .cg-cmd{display:flex;align-items:center;justify-content:space-between;gap:12px;background:#0a0a0b;
@@ -9083,6 +9174,14 @@ function bootDisconnected(info){
9083
9174
  #connectGate .cg-hint{font-size:12px;line-height:1.6;color:var(--ink3,#6b6862);margin:0 0 16px}
9084
9175
  #connectGate .cg-hint a{color:var(--blue,#8fb0ff);text-decoration:none}
9085
9176
  #connectGate .cg-hint a:hover{text-decoration:underline}
9177
+ #connectGate .cg-trust{margin:18px 0 0;padding:16px 0 0;border-top:1px solid var(--hair2,rgba(255,255,255,.10))}
9178
+ #connectGate .cg-blanket{font-size:12.5px;line-height:1.6;color:var(--ink,#e9e7e2);font-weight:600;margin:0}
9179
+ #connectGate .cg-acchead{font:500 10px/1 var(--mono,monospace);letter-spacing:.09em;text-transform:uppercase;color:var(--ink3,#6b6862);margin:16px 0 6px}
9180
+ #connectGate .cg-accrow{font-size:12px;line-height:1.45;color:var(--ink2,#a3a097);padding:0 0 0 12px;margin:0 0 6px;position:relative}
9181
+ #connectGate .cg-accrow:before{content:'';position:absolute;left:0;top:7px;width:4px;height:1px;background:var(--ink3,#6b6862);opacity:.7}
9182
+ #connectGate .cg-seclink{margin:14px 0 0;font-size:12px}
9183
+ #connectGate .cg-seclink a{color:var(--blue,#8fb0ff);text-decoration:none}
9184
+ #connectGate .cg-acct{margin:18px 0 0;padding:16px 0 0;border-top:1px solid var(--hair2,rgba(255,255,255,.10))}
9086
9185
  #connectGate .cg-who{font-size:12px;color:var(--ink3,#6b6862);margin:0 0 4px}
9087
9186
  #connectGate .cg-who b{color:var(--ink2,#a3a097)}
9088
9187
  #connectGate .cg-switch{font-size:12px;color:var(--ink3,#6b6862);margin:14px 0 0;transition:color .2s}
@@ -9097,6 +9196,7 @@ function bootDisconnected(info){
9097
9196
  #connectGate .cg-switch.escalated{color:var(--danger);font-weight:600}
9098
9197
  #connectGate .cg-switch.escalated a{color:var(--danger);text-decoration:underline}
9099
9198
  #connectGate .cg-x{position:absolute;top:14px;right:16px;width:28px;height:28px;border:0;background:transparent;color:var(--ink3,#6b6862);font-size:20px;line-height:1;cursor:pointer}
9199
+ #connectGate .cg-waitwrap{display:flex;justify-content:center}
9100
9200
  #connectGate .cg-wait{display:inline-flex;align-items:center;gap:7px;font:500 10.5px/1 var(--mono,monospace);letter-spacing:.08em;color:var(--ink3,#6b6862);margin-top:16px}
9101
9201
  #connectGate .cg-wait i{width:6px;height:6px;border-radius:50%;background:var(--green,#3DDC97);animation:cgpulse 1.8s ease-in-out infinite}
9102
9202
  @keyframes cgpulse{0%,100%{box-shadow:0 0 0 0 color-mix(in srgb,var(--green,#3DDC97) 50%,transparent)}50%{box-shadow:0 0 6px 2px color-mix(in srgb,var(--green,#3DDC97) 35%,transparent)}}
@@ -9116,9 +9216,21 @@ function bootDisconnected(info){
9116
9216
  + '<a href="https://claude.com/claude-code" target="_blank" rel="noreferrer">Claude Code</a> or '
9117
9217
  + '<a href="https://openai.com/codex/" target="_blank" rel="noreferrer">Codex</a>, then run the same command '
9118
9218
  + '— Galda uses whichever agent you have.</p>'
9219
+ // The gate said what to run and never what it would do. "npx? what else is
9220
+ // it going to touch?" is the actual reason people stop here, so the answer
9221
+ // sits with the command. Facts only, in step with docs/SECURITY.md.
9222
+ + '<div class="cg-trust">'
9223
+ + '<p class="cg-blanket">No admin access, no background service, nothing that starts on its own. '
9224
+ + 'Plain JavaScript you can read before you run it, and one folder you can delete.</p>'
9225
+ + '<div class="cg-acchead">What it can access</div>'
9226
+ + NPX_ONBOARDING.access.map(function(t){ return '<div class="cg-accrow">' + esc(t) + '</div>'; }).join('')
9227
+ + '<p class="cg-seclink"><a href="https://galda.app/security" target="_blank" rel="noreferrer">Security and permissions \u2192</a></p>'
9228
+ + '</div>'
9229
+ + '<div class="cg-acct">'
9119
9230
  + '<p class="cg-who">Signed in as <b></b></p>'
9120
9231
  + '<p class="cg-switch">Already running Galda under a different account? Run <code>npx @galda/cli --signin</code> and pick this one. Or <a href="/logout">switch Google account →</a></p>'
9121
- + '<div class="cg-wait"><i></i>WAITING FOR THIS DEVICE</div></div>';
9232
+ + '</div>'
9233
+ + '<div class="cg-waitwrap"><div class="cg-wait"><i></i>WAITING FOR THIS DEVICE</div></div></div>';
9122
9234
  document.body.appendChild(ov);
9123
9235
  ov.querySelector('.cg-who b').textContent = email || 'your account';
9124
9236
  const open = () => ov.classList.add('on');
@@ -9323,6 +9435,25 @@ function typeChatTurn(turn){
9323
9435
  if (turn.revealed >= full.length) { turn.done = true; stopChatTyper(); renderFsFeed(); }
9324
9436
  }, 16);
9325
9437
  }
9438
+ // A worker's opening words (engine: `ev:'say'`). Rendered as a spoken turn, typed
9439
+ // out, exactly like the one-shot chat reply — Galda writes nothing of its own
9440
+ // here and picks nothing: the sentence is the agent's, and this only puts it
9441
+ // where it can be read. The board layout has no centre feed, so it stays in the
9442
+ // run log there rather than being invented somewhere new.
9443
+ function sayFromAgent({ goalId, text, agent }){
9444
+ const body = String(text ?? '').trim();
9445
+ if (!body || state.layout !== 'flagship') return;
9446
+ const goal = state.goals.find((g) => g.id === goalId);
9447
+ const projectId = goal?.projectId ?? state.active;
9448
+ const turns = (state.chatTurns[projectId] ||= []);
9449
+ // A reconnecting stream can hand us the same opening twice; a person reading it
9450
+ // twice would think the agent had started over.
9451
+ if (turns.some((t) => t.role === 'clawd' && t.text === body)) return;
9452
+ const turn = { role: 'clawd', text: body, revealed: 0, done: false, agent, ts: Date.now() };
9453
+ turns.push(turn);
9454
+ renderFsFeed();
9455
+ if (projectId === state.active) typeChatTurn(turn); else { turn.revealed = body.length; turn.done = true; }
9456
+ }
9326
9457
  // A new send starts: finish any in-flight reply cleanly (complete the model turn +
9327
9458
  // drop the caret, hide the board bar) so a half-typed reply never lingers.
9328
9459
  function finalizeChatTyping(){
@@ -11510,7 +11641,7 @@ function renderCtxBar(){
11510
11641
  .join('');
11511
11642
  // Redrawing the menu must not hand back a second chooser: carry the disabled
11512
11643
  // state that setFolderPickerBusy() put on the live button.
11513
- const openRow = `<button type="button" class="appopt ctxopen${folderPickerBusy ? ' busy' : ''}" id="ctxopenfolder"${folderPickerBusy ? ' disabled' : ''} title="Open the OS folder chooser"><svg viewBox="0 0 24 24"><path d="M3 7a2 2 0 012-2h4l2 2h8a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z"/><path d="M12 11v6M9 14h6"/></svg><span class="nm">Open folder…</span></button>`;
11644
+ const openRow = `<button type="button" class="appopt ctxopen${folderPickerBusy ? ' busy' : ''}" id="ctxopenfolder"${folderPickerBusy ? ' disabled' : ''} title="Open the OS folder chooser">${folderRowInner(folderPickerBusy)}</button>`;
11514
11645
  menu.innerHTML = `${opts}${openRow}<div class="ctxmsg" id="ctxdirmsg"></div>`;
11515
11646
  }
11516
11647
  // Workspace mode is fixed to the isolated worktree (clean separate checkout). The
@@ -11562,17 +11693,37 @@ async function setProjectFolder(dir){
11562
11693
  // request stays in flight for as long as the dialog is up, so "a request is out"
11563
11694
  // is exactly "a dialog is open" — no timer to guess with.
11564
11695
  let folderPickerBusy = false;
11696
+ const FOLDER_GLYPH = '<svg viewBox="0 0 24 24"><path d="M3 7a2 2 0 012-2h4l2 2h8a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z"/><path d="M12 11v6M9 14h6"/></svg>';
11697
+ // While a chooser is open the icon spins in its own place. Waiting with nothing
11698
+ // moving is what "it did nothing" looks like — and it is what made Masa's friend
11699
+ // tap ten times (2026-07-31). One source for the row's contents, so the live swap
11700
+ // and the next redraw cannot disagree.
11701
+ function folderRowInner(busy){
11702
+ return `${busy ? '<span class="ctxspin" aria-hidden="true"></span>' : FOLDER_GLYPH}<span class="nm">Open folder…</span>`;
11703
+ }
11565
11704
  function setFolderPickerBusy(busy){
11566
11705
  folderPickerBusy = busy;
11567
11706
  const btn = $('ctxopenfolder');
11568
- if (btn) { btn.disabled = busy; btn.classList.toggle('busy', busy); }
11569
- }
11707
+ if (btn) { btn.disabled = busy; btn.classList.toggle('busy', busy); btn.innerHTML = folderRowInner(busy); }
11708
+ }
11709
+ // How long to wait before saying the dialog might not be where they are looking.
11710
+ // The engine opens it immediately; on a Mac where the engine was started by launchd
11711
+ // it can open behind everything, and there is nothing on screen to say so — which is
11712
+ // the other half of what happened to Masa's friend. Long enough not to nag someone
11713
+ // who is simply choosing a folder.
11714
+ const FOLDER_CHOOSER_SLOW_MS = 6000;
11570
11715
  async function openFolderNative(){
11571
11716
  if (folderPickerBusy) return;
11572
11717
  const msg = $('ctxdirmsg');
11573
11718
  const say = (t, bad) => { if (msg) { msg.textContent = t; msg.classList.toggle('bad', Boolean(bad)); } };
11574
11719
  setFolderPickerBusy(true);
11575
11720
  say('Opening the folder chooser…');
11721
+ // Still open after a while: say where it might be, and point at the way out that
11722
+ // does not need it. The row stays disabled — a second chooser is the bug this
11723
+ // guard exists to prevent, and offering one here would re-create it.
11724
+ const slow = setTimeout(() => {
11725
+ if (folderPickerBusy) say('Still waiting — the chooser may be behind your other windows. You can also pick a folder from the list above.');
11726
+ }, FOLDER_CHOOSER_SLOW_MS);
11576
11727
  try {
11577
11728
  const r = await fetch(withKey('/api/choose-folder'), { method: 'POST' });
11578
11729
  const body = await r.json().catch(() => ({}));
@@ -11581,7 +11732,7 @@ async function openFolderNative(){
11581
11732
  if (body.unsupported) { say('This engine can’t open a folder dialog on this system.', true); return; }
11582
11733
  say(body.error || 'Could not open the folder chooser.', true);
11583
11734
  } catch { say('Could not reach the engine.', true); }
11584
- finally { setFolderPickerBusy(false); }
11735
+ finally { clearTimeout(slow); setFolderPickerBusy(false); }
11585
11736
  }
11586
11737
  $('ctxdirbtn').onclick = (e) => {
11587
11738
  e.stopPropagation();
@@ -11866,6 +12017,13 @@ $('histBtn').onclick = openHistory;
11866
12017
  $('setBtn').onclick = openSettings;
11867
12018
  $('histClose').onclick = () => $('historyOverlay').classList.remove('show');
11868
12019
  $('setClose').onclick = () => $('settingsOverlay').classList.remove('show');
12020
+ // About panel: Close, tapping the backdrop, and Esc all dismiss it (DESIGN-RULES
12021
+ // §設定画面 — a dialog must close when you click somewhere unrelated).
12022
+ $('aboutClose').onclick = () => closeAbout();
12023
+ $('aboutOverlay').onclick = (e) => { if (e.target === $('aboutOverlay')) closeAbout(); };
12024
+ document.addEventListener('keydown', (e) => {
12025
+ if (e.key === 'Escape' && $('aboutOverlay')?.classList.contains('show')) { e.stopPropagation(); closeAbout(); }
12026
+ }, true);
11869
12027
  $('historyOverlay').addEventListener('click', (e) => { if (e.target === $('historyOverlay')) $('historyOverlay').classList.remove('show'); });
11870
12028
  $('settingsOverlay').addEventListener('click', (e) => { if (e.target === $('settingsOverlay')) $('settingsOverlay').classList.remove('show'); });
11871
12029
  for (const b of $('langSeg').querySelectorAll('button')) b.onclick = () => saveLang(b.dataset.lang);
@@ -12517,7 +12675,13 @@ window.__tbink = (i) => { document.body.dataset.tbink = String(((i | 0) % 3 + 3)
12517
12675
  raf = 0;
12518
12676
  if (document.hidden) { clearFx(); return; }
12519
12677
  const t = (performance.now() - t0) / 1000, d = Math.min(2, devicePixelRatio || 1);
12520
- fc.setTransform(d, 0, 0, d, 0, 0); fc.clearRect(0, 0, innerWidth, innerHeight);
12678
+ // Clear in backing-store pixels *before* installing the CSS-pixel transform.
12679
+ // Mobile Safari can change its visual viewport as a question answer removes a
12680
+ // card (or as the browser chrome expands). Clearing in transformed viewport
12681
+ // coordinates then leaves pixels from the send ring's former location behind.
12682
+ // The canvas's own dimensions are the only stable full-clear bounds.
12683
+ clearFx();
12684
+ fc.setTransform(d, 0, 0, d, 0, 0);
12521
12685
  if (MODE < 10) tick(t); // TB edge anim (optional, off by default = 10)
12522
12686
  drawSendRing(t); // send ring (always on while btn mode = 0, independent of TB edge anim)
12523
12687
  raf = requestAnimationFrame(loop);
@@ -12676,11 +12840,133 @@ function gearSetHTML(){
12676
12840
  <button class="optchip${lang === 'en' ? ' on' : ''}" data-gslang="en">EN</button>
12677
12841
  <button class="optchip${lang === 'ja' ? ' on' : ''}" data-gslang="ja">JA</button></div>
12678
12842
  ${billingRow}
12843
+ <div class="srow"><div class="sl">About Galda<span class="sd">What it is, what the agent can touch, and what leaves this machine</span></div>
12844
+ <button class="optchip" data-gabout="1">Open</button></div>
12845
+ ${remoteAccessRow()}
12679
12846
  ${stability}
12680
12847
  ${gearRulesHTML()}
12681
12848
  ${hostReadinessRow()}
12682
12849
  <div class="srow dis"><div class="sl">Data folder<span class="sd">Runs are stored in the engine's chat-runs folder on the host</span></div></div>`;
12683
12850
  }
12851
+ // The npx moment, moved to where the intent is.
12852
+ //
12853
+ // A person who signed up on the web lands on a board with nothing behind it:
12854
+ // Galda drives the Claude Code / Codex on THEIR machine, and that is not
12855
+ // running yet. Until now the only thing they were told was "Galda is offline —
12856
+ // reconnecting…", which is true and useless: it describes our state, not their
12857
+ // next step. That is the "run npx" dead end.
12858
+ //
12859
+ // So the answer arrives when they reach for the composer, which is the moment
12860
+ // they have decided to do something. And it has to answer the real hesitation,
12861
+ // not just print a command: "npx? what else is it going to do to my machine?"
12862
+ //
12863
+ // Every line below is a fact about the shipped package, kept in step with
12864
+ // docs/SECURITY.md and the same block on galda.app. Do not promise here what
12865
+ // is not true there.
12866
+ const NPX_ONBOARDING = {
12867
+ why: 'This is a command, not a website, because Galda runs the Claude Code or Codex already installed on your machine, on your own subscription. A website cannot reach them.',
12868
+ blanket: 'No admin access, no background service, nothing that starts on its own. Plain JavaScript you can read before you run it, and one folder you can delete.',
12869
+ access: [
12870
+ 'Your project files and tests, inside a throwaway copy',
12871
+ 'Its git is read-only: no commit, no push, no checkout, no reset',
12872
+ 'Never your working tree, your branch, or your uncommitted changes',
12873
+ 'A fixed allowlist of commands, not arbitrary shell',
12874
+ 'Nothing at all at install time',
12875
+ ],
12876
+ };
12877
+
12878
+ // The nudge: with no agent behind the board, point at the composer instead of
12879
+ // leaving a new person to guess. Only while there is nothing to do here — it
12880
+ // disappears the moment an agent connects.
12881
+ function renderComposerNudge(){
12882
+ const n = $('composerNudge');
12883
+ if (!n) return;
12884
+ // Only while there is genuinely nothing here. The moment a first goal exists
12885
+ // the board explains itself and a standing instruction becomes clutter.
12886
+ const empty = !(state.goals || []).length && !(state.tasks || []).length;
12887
+ n.classList.toggle('show', empty);
12888
+ }
12889
+
12890
+ // The orientation panel's content. A constant, not a request: this renders with
12891
+ // zero tokens and no network, because the answer never depends on the project.
12892
+ //
12893
+ // Every line here is a fact about the shipped code, kept in step with
12894
+ // docs/SECURITY.md. Do not add a promise here that is not true there.
12895
+ const ABOUT_GALDA = [
12896
+ { h: 'What Galda is', p: [
12897
+ 'Galda hands your work to Claude Code or Codex and brings back something you can review.',
12898
+ 'It has no AI of its own. It starts the agent already installed and signed in on this machine, on your own subscription, so there is no extra API cost — and your code is never uploaded to Galda to be run through a model.',
12899
+ ] },
12900
+ { h: 'What the agent can do', p: [
12901
+ 'Read and edit files, and run tests, inside a private copy of your project.',
12902
+ 'Run named commands only — language runtimes, package managers, test and lint runners, and read-only shell tools. Not arbitrary shell.',
12903
+ ] },
12904
+ { h: 'What the agent cannot do', p: [
12905
+ 'It cannot commit, push, checkout, or reset. Its git access is read-only: status, log, diff, show, branch, blame, ls-files, rev-parse. Nothing else.',
12906
+ 'It cannot touch your working tree. Every task runs in a throwaway git worktree, so your branch and your uncommitted changes are left alone.',
12907
+ ] },
12908
+ { h: 'What leaves this machine', p: [
12909
+ 'Model traffic goes from Claude Code or Codex straight to Anthropic or OpenAI. Galda is not in that path.',
12910
+ 'With remote access on, what this board displays passes through Galda’s relay so you can open it from your phone. It is encrypted in transit but not end to end, so treat Galda’s server as being in the path.',
12911
+ 'Turn remote access off in Settings and no outbound connection is opened at all.',
12912
+ ] },
12913
+ { h: 'Removing it', p: [
12914
+ 'Galda installs no background services and runs nothing at install time. Deleting ~/.manager-for-ai removes everything it keeps.',
12915
+ ] },
12916
+ ];
12917
+ function openAbout(){
12918
+ const body = $('aboutBody');
12919
+ if (body) {
12920
+ body.innerHTML = ABOUT_GALDA.map((s) => `
12921
+ <div class="setrow" style="display:block">
12922
+ <div class="setlabel">${esc(s.h)}</div>
12923
+ ${s.p.map((t) => `<div class="setsub" style="margin-top:4px">${esc(t)}</div>`).join('')}
12924
+ </div>`).join('');
12925
+ }
12926
+ $('aboutOverlay')?.classList.add('show');
12927
+ }
12928
+ function closeAbout(){ $('aboutOverlay')?.classList.remove('show'); }
12929
+ // Reachable from the first-run composer link too, once that lands.
12930
+ window.openAbout = openAbout;
12931
+
12932
+ // Remote access = the relay. On, you can open this board from your phone; the
12933
+ // price is that what the board shows crosses Galda's relay server (it is a
12934
+ // reverse HTTP tunnel, TLS but not end-to-end encrypted — docs/SECURITY.md).
12935
+ // Off, no outbound socket is opened at all.
12936
+ //
12937
+ // This row exists because that switch already worked and nobody could find it:
12938
+ // it was the env var `RELAY_URL=`. The people who most need it are the ones who
12939
+ // will never read a source comment. Cursor puts Privacy Mode in Settings for
12940
+ // the same reason. Same shape as Language above — a server setting that applies
12941
+ // the moment it is chosen, not part of the visual edit session.
12942
+ //
12943
+ // No row at all when this build has no relay configured (§3 段階開示: never show
12944
+ // a switch that controls nothing).
12945
+ function remoteAccessRow(){
12946
+ const rp = state.relayPref;
12947
+ if (!rp || !rp.configured) return '';
12948
+ const on = rp.enabled === true;
12949
+ const desc = on
12950
+ ? 'On — reach this board from your phone. What the board shows passes through Galda’s relay.'
12951
+ : 'Off — nothing leaves this machine. This board is not reachable from your phone.';
12952
+ return `<div class="srow"><div class="sl">Remote access<span class="sd">${esc(desc)}</span></div>
12953
+ <button class="optchip${on ? ' on' : ''}" data-grelay="on">On</button>
12954
+ <button class="optchip${on ? '' : ' on'}" data-grelay="off">Off</button></div>`;
12955
+ }
12956
+ async function loadRelayPref(){
12957
+ try { const r = await fetch(withKey('/api/relay-pref')); state.relayPref = r.ok ? await r.json() : null; }
12958
+ catch { state.relayPref = null; }
12959
+ if ($('gearPop') && custTab === 'settings') buildGearPop();
12960
+ }
12961
+ async function saveRelayPref(enabled){
12962
+ const r = await fetch(withKey('/api/relay-pref'), {
12963
+ method: 'POST', headers: { 'content-type': 'application/json' },
12964
+ body: JSON.stringify({ enabled }),
12965
+ });
12966
+ if (!r.ok) return showErr('Failed to save.');
12967
+ state.relayPref = await r.json();
12968
+ if ($('gearPop').classList.contains('open')) buildGearPop();
12969
+ }
12684
12970
  async function loadStability(){
12685
12971
  try { const r = await fetch(withKey('/api/stability')); state.stability = r.ok ? await r.json() : null; }
12686
12972
  catch { state.stability = null; }
@@ -12772,6 +13058,8 @@ function buildGearPop(){
12772
13058
  pop.innerHTML = gearTabsHTML() + gearSetHTML();
12773
13059
  wireGearCommon(pop);
12774
13060
  for (const b of pop.querySelectorAll('[data-gslang]')) b.onclick = () => saveLang(b.dataset.gslang);
13061
+ for (const b of pop.querySelectorAll('[data-grelay]')) b.onclick = () => saveRelayPref(b.dataset.grelay === 'on');
13062
+ for (const b of pop.querySelectorAll('[data-gabout]')) b.onclick = () => openAbout();
12775
13063
  const signBtn = pop.querySelector('[data-gsignin]');
12776
13064
  if (signBtn) signBtn.onclick = async () => {
12777
13065
  const msg = pop.querySelector('#licKeyMsg');
@@ -12797,6 +13085,7 @@ function buildGearPop(){
12797
13085
  if (stabilityBtn) stabilityBtn.onclick = () => { state.stability = null; buildGearPop(); loadStability(); };
12798
13086
  loadProjectRules();
12799
13087
  if (!state.stability) loadStability();
13088
+ if (!state.relayPref) loadRelayPref();
12800
13089
  return;
12801
13090
  }
12802
13091
  if (custTab === 'mcp') {
package/engine/lib.mjs CHANGED
@@ -1869,8 +1869,8 @@ export function reorderQueue(waiting, ids) {
1869
1869
  return [...orderByIds(queued, ids), ...rest];
1870
1870
  }
1871
1871
 
1872
- // Priority queue ordering: '' runs before '' before ''. A task with no
1873
- // priority (or an unrecognized value) is treated as '', matching the
1872
+ // Priority queue ordering: 'high' runs before 'medium' before 'low'. A task with
1873
+ // no priority (or an unrecognized value) is treated as 'medium', matching the
1874
1874
  // default new tasks are created with. Array.prototype.sort is a stable sort
1875
1875
  // (guaranteed by the spec), so this never disturbs the relative order tasks
1876
1876
  // already have within the same priority — that incoming order already
@@ -1879,12 +1879,39 @@ export function reorderQueue(waiting, ids) {
1879
1879
  // for everything it doesn't touch). Callers should feed this the array
1880
1880
  // *after* applying a drag reorder, so a drag only ever re-ranks within a
1881
1881
  // priority band rather than fighting it.
1882
- export const TASK_PRIORITIES = ['高', '中', '低'];
1883
- const PRIORITY_WEIGHT = { 高: 0, 中: 1, 低: 2 };
1882
+ //
1883
+ // The values were 高/中/低 until 2026-07-31 (Masa: the UI writes its own labels
1884
+ // in English — DESIGN-RULES §5.5). They are stored, sorted on, and sent over the
1885
+ // API, so the old ones cannot simply be swapped out: every task written before
1886
+ // today still says 高 on disk, and a browser tab left open on the old app will
1887
+ // keep PUTting 高 for as long as it stays open. Both are accepted forever and
1888
+ // normalised on the way in; nothing has to be migrated, and nothing that already
1889
+ // exists loses its place in the queue.
1890
+ export const TASK_PRIORITIES = ['high', 'medium', 'low'];
1891
+ const LEGACY_PRIORITIES = { 高: 'high', 中: 'medium', 低: 'low' };
1892
+ export const DEFAULT_PRIORITY = 'medium';
1893
+
1894
+ export function normalizePriority(value) {
1895
+ const v = String(value ?? '').trim();
1896
+ if (TASK_PRIORITIES.includes(v)) return v;
1897
+ if (LEGACY_PRIORITIES[v]) return LEGACY_PRIORITIES[v];
1898
+ const lower = v.toLowerCase();
1899
+ if (TASK_PRIORITIES.includes(lower)) return lower; // 'High' from a hand-written call
1900
+ return DEFAULT_PRIORITY;
1901
+ }
1902
+
1903
+ // Whether a value is one this product accepts at all — the API's gate. Deliberately
1904
+ // wider than TASK_PRIORITIES: an old client sending 高 is not making a mistake.
1905
+ export function isAcceptedPriority(value) {
1906
+ const v = String(value ?? '').trim();
1907
+ return TASK_PRIORITIES.includes(v) || Boolean(LEGACY_PRIORITIES[v]) || TASK_PRIORITIES.includes(v.toLowerCase());
1908
+ }
1909
+
1910
+ const PRIORITY_WEIGHT = { high: 0, medium: 1, low: 2 };
1884
1911
  export function sortQueueByPriority(tasks) {
1885
1912
  return tasks
1886
1913
  .map((t, i) => ({ t, i }))
1887
- .sort((a, b) => (PRIORITY_WEIGHT[a.t.priority] ?? 1) - (PRIORITY_WEIGHT[b.t.priority] ?? 1) || a.i - b.i)
1914
+ .sort((a, b) => (PRIORITY_WEIGHT[normalizePriority(a.t.priority)] ?? 1) - (PRIORITY_WEIGHT[normalizePriority(b.t.priority)] ?? 1) || a.i - b.i)
1888
1915
  .map(({ t }) => t);
1889
1916
  }
1890
1917
 
@@ -3270,7 +3297,7 @@ export function buildGoalTask(goal, { id, num, passCondition = null, createdAt }
3270
3297
  mode: goal.mode ?? 'auto', skill: goal.skill ?? null,
3271
3298
  workspaceMode: resolveWorkspaceMode(goal.workspaceMode ?? goal.worktree),
3272
3299
  worktree: goal.worktree === false ? false : undefined,
3273
- status: 'queued', createdAt, priority: '中',
3300
+ status: 'queued', createdAt, priority: DEFAULT_PRIORITY,
3274
3301
  result: null, changedFiles: [], secs: null, activity: [], proof: null, usage: null,
3275
3302
  };
3276
3303
  }
package/engine/server.mjs CHANGED
@@ -21,7 +21,7 @@ import { homedir } from 'node:os';
21
21
  import { fileURLToPath } from 'node:url';
22
22
  import { pathToFileURL } from 'node:url';
23
23
  import { bootBanner, bootDestination, 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
- 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';
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, DEFAULT_PRIORITY, normalizePriority, isAcceptedPriority, 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';
27
27
  import { readRelayPref, writeRelayPref, resolveRelayEnabled } from './relay-pref.mjs';
@@ -2357,7 +2357,7 @@ function queueReplyTask(goal, text, { from = 'manager', via = 'composer' } = {})
2357
2357
  title: text.replace(/\s+/g, ' ').slice(0, 60), detail: promptText.slice(0, 4000), replyFrom: from,
2358
2358
  workspaceMode: resolveWorkspaceMode(goal.workspaceMode ?? goal.worktree),
2359
2359
  worktree: goal.worktree === false ? false : undefined,
2360
- passCondition: null, status: 'queued', createdAt: new Date().toISOString(), priority: '中',
2360
+ passCondition: null, status: 'queued', createdAt: new Date().toISOString(), priority: DEFAULT_PRIORITY,
2361
2361
  result: null, changedFiles: [], secs: null, activity: [], proof: null, usage: null,
2362
2362
  userRequirementHistory: tasks.filter((t) => t.goalId === goal.id && (!t.reply || t.replyFrom === 'you')).slice(-12).map((t) => ({ text: t.reply ? t.detail : t.title, status: t.status })),
2363
2363
  // Transport the prior attempts as facts; the worker, not Manager, decides
@@ -4921,11 +4921,22 @@ const server = createServer(async (req, res) => {
4921
4921
  // guarded here.
4922
4922
  const runnable = !pending && !review && !approval;
4923
4923
  const lang = /[぀-ヿ㐀-鿿]/.test(text) ? 'ja' : 'en';
4924
- // Task 5 (PR運用ヒアリング): ask once per project, before its first
4925
- // runnable goal whether this project wants PRs.
4926
- const needsReviewPref = runnable && needsReviewPreference({ askedBefore: project.reviewPrefAsked });
4927
- const reviewPrefQuestion = needsReviewPref ? buildReviewPreferenceQuestion({ lang }) : undefined;
4928
- if (needsReviewPref) { project.reviewPrefAsked = true; saveProjects(); }
4924
+ // Galda no longer asks "do you want PRs?" (Masa 2026-07-31). It used to ask once
4925
+ // per project, before the first runnable goal, WITHOUT looking at what was asked —
4926
+ // needsReviewPreference() was `return !askedBefore`. A first-time user typing
4927
+ // 「テスト」 got interrogated about pull requests, and because the folder question
4928
+ // had claimed the needsInput slot on message 1, it landed on message 2, out of
4929
+ // nowhere. (That folder question is gone too, as of #642 — this is the other half
4930
+ // of the same interrogation.)
4931
+ //
4932
+ // Nothing needs the answer: resolveWantsPR() already falls back to
4933
+ // `!preferenceKnown && isImplementationRequest(text)`, i.e. a PR for work that looks
4934
+ // like implementation and none for 「テスト」 — which is the behaviour the question
4935
+ // was there to obtain. Projects that already answered keep their setting, and an
4936
+ // agent that genuinely needs to know can ask for itself: the question layer belongs
4937
+ // to Claude Code / Codex, not to Galda.
4938
+ const needsReviewPref = false;
4939
+ const reviewPrefQuestion = undefined;
4929
4940
  // Clamp to whatever agent CLIs are actually installed on this machine
4930
4941
  // (connected-agents Phase A) — a requested/default agent that isn't
4931
4942
  // present would otherwise spawn a worker doomed to fail at launch.
@@ -5317,7 +5328,7 @@ const server = createServer(async (req, res) => {
5317
5328
  return;
5318
5329
  }
5319
5330
 
5320
- // change a queued task's priority (高/中/低) — re-sorts its project's queue
5331
+ // change a queued task's priority (high/medium/low) — re-sorts its project's queue
5321
5332
  // so a bumped-up task actually runs sooner, not just displays that way.
5322
5333
  const priorityMatch = url.pathname.match(/^\/api\/tasks\/(\d+)\/priority$/);
5323
5334
  if (priorityMatch && req.method === 'PUT') {
@@ -5329,8 +5340,11 @@ const server = createServer(async (req, res) => {
5329
5340
  req.on('end', () => {
5330
5341
  try {
5331
5342
  const { priority } = JSON.parse(body);
5332
- if (!TASK_PRIORITIES.includes(priority)) return json(res, 400, { error: `priority must be one of: ${TASK_PRIORITIES.join(', ')}` });
5333
- task.priority = priority;
5343
+ // A browser left open on yesterday's app still sends 高/中/低. Refusing it
5344
+ // would break a tab nobody reloaded, for a rename that is ours, so the old
5345
+ // words are accepted and stored as the new ones.
5346
+ if (!isAcceptedPriority(priority)) return json(res, 400, { error: `priority must be one of: ${TASK_PRIORITIES.join(', ')}` });
5347
+ task.priority = normalizePriority(priority);
5334
5348
  saveTask(task);
5335
5349
  const q = queues.get(task.projectId);
5336
5350
  if (q) { q.waiting = sortQueueByPriority(q.waiting); for (const t of q.waiting) saveTask(t); }
@@ -5506,9 +5520,26 @@ const server = createServer(async (req, res) => {
5506
5520
  const existing = projects.find((p) => p.dir === dir);
5507
5521
  let proj = existing;
5508
5522
  if (!proj) {
5509
- proj = { id: uniqueProjectId(basename(dir)), name: basename(dir), dir };
5510
- projects.push(proj); saveProjects();
5511
- queues.set(proj.id, { running: new Set(), waiting: [] });
5523
+ // The free tier allows one project, and POST /api/projects enforces that — but
5524
+ // this path never did, so every folder someone named here minted another one
5525
+ // (measured 2026-07-31: a first-time user on the free tier ended up with three).
5526
+ // At the cap, "pick a folder" means "this is the folder I work in", so point the
5527
+ // project someone already has at it instead of inventing a second.
5528
+ const atCap = checkFreeTierLimit({ ...computeFreeTierUsage(goal.owner ?? 'owner'), projectCount: projects.length + 1 }).reason === 'project-limit';
5529
+ // Only ever the goal's OWN project. A folder question only fires when that
5530
+ // project's dir is HOME or not a repo, so it is by definition the unusable one
5531
+ // and pointing it at the chosen folder is what the person just asked for. Never
5532
+ // repoint somebody else's project to make room.
5533
+ const reusable = atCap ? projects.find((p) => p.id === goal.projectId) : null;
5534
+ if (reusable) {
5535
+ reusable.dir = dir;
5536
+ saveProjects();
5537
+ proj = reusable;
5538
+ } else {
5539
+ proj = { id: uniqueProjectId(basename(dir)), name: basename(dir), dir };
5540
+ projects.push(proj); saveProjects();
5541
+ queues.set(proj.id, { running: new Set(), waiting: [] });
5542
+ }
5512
5543
  }
5513
5544
  goal.projectId = proj.id;
5514
5545
  goal.question = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@galda/cli",
3
- "version": "0.10.118",
3
+ "version": "0.10.120",
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": {