@galda/cli 0.10.105 → 0.10.107

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
@@ -877,6 +877,23 @@
877
877
  reviewed goal has no g.pr — one card per requirement (task) with its
878
878
  proof video, Approve/Dismiss, and a revision-request chat. */
879
879
  .reviewmodal{width:640px;max-height:74vh;overflow-y:auto}
880
+ /* goal-806/#851 案3: 複数レビュー内でどのタスクを見ているか掴めるよう、左に
881
+ 細い番号レール。IntersectionObserver でスクロール位置に連動してハイライトし、
882
+ クリックで気持ちよく(smooth)その見出し頭へジャンプできる。1件しか
883
+ requirement が無いレビューでは要らないので JS 側で .show を切り替える。 */
884
+ .reviewmodal{display:flex;align-items:flex-start;gap:0}
885
+ .revrail{position:sticky;top:0;align-self:flex-start;flex:0 0 auto;width:0;overflow:hidden;
886
+ display:flex;flex-direction:column;align-items:center;padding-top:1px}
887
+ .revrail.show{width:30px;margin-right:12px}
888
+ .revrail button{all:unset;box-sizing:border-box;cursor:pointer;display:flex;flex-direction:column;align-items:center;
889
+ gap:4px;padding:7px 0;width:100%;color:var(--ink3);font:600 10px/1 var(--mono);transition:color .15s}
890
+ .revrail button .bar{width:3px;height:16px;border-radius:2px;background:var(--hair2);transition:background .15s,height .15s}
891
+ .revrail button.on{color:var(--ink)}
892
+ .revrail button.on .bar{background:var(--green);height:24px}
893
+ /* task-971: 白黒(モノクロ)版 — ?railmono=1 で緑を使わずinkの濃淡だけで
894
+ 現在地を示す比較用バリアント。配色を使わない代替案の見え方を確認するため。 */
895
+ .revrail.mono button.on .bar{background:var(--ink)}
896
+ .revbody{flex:1;min-width:0}
880
897
  .revsummary{margin:-4px 0 12px}
881
898
  /* task 96: recap block shared by the checklist top summary and the chat导线 */
882
899
  .rvhead{display:flex;align-items:center;gap:6px;font-family:var(--mono);font-size:11px;color:var(--ink2);font-weight:700}
@@ -953,7 +970,7 @@
953
970
  @keyframes upcelflash{0%{transform:scale(.4);opacity:.95}100%{transform:scale(3);opacity:0}}
954
971
  @media (prefers-reduced-motion:reduce){ .upcel.show .upcelflash{animation:none;opacity:0} }
955
972
  .reqlist{display:flex;flex-direction:column;gap:12px}
956
- .reqcard{border:1px solid var(--hair2);border-radius:14px;padding:14px 16px;background:var(--paper);transition:border-color .15s,background-color .15s}
973
+ .reqcard{border:1px solid var(--hair2);border-radius:14px;padding:14px 16px;background:var(--paper);transition:border-color .15s,background-color .15s;scroll-margin-top:8px}
957
974
  .reqcard.approved{border-color:rgba(var(--green-rgb),.5);background:rgba(var(--green-rgb),.05)}
958
975
  .reqcard.dismissed{border-color:rgba(var(--danger-rgb),.45);background:rgba(var(--danger-rgb),.05)}
959
976
  .reqhead{display:flex;align-items:baseline;gap:8px}
@@ -1080,7 +1097,7 @@
1080
1097
  sentence-case UI section heads, 14px --ink body prose, canvas log box. Same
1081
1098
  IDs/classes/markup as before — ONLY the look changes. Behaviour is unchanged
1082
1099
  (Approve stays hidden for failed; failure brief / Archive / send-back shown).
1083
- All tokens (--panel/--hair/--ink*/--canvas/--ui/--mono) so it adapts per theme. */
1100
+ All tokens (--panel/--hair/--ink family/--canvas/--ui/--mono) so it adapts per theme. */
1084
1101
  .gdoverlay .modal{width:min(680px,94%);max-height:min(86vh,760px);display:flex;flex-direction:column;position:relative;overflow:hidden;
1085
1102
  background:var(--panel);border:0;border-radius:18px;padding:0;
1086
1103
  box-shadow:0 18px 54px -18px rgba(15,17,40,.30),0 0 0 1px var(--hair);
@@ -1581,16 +1598,19 @@
1581
1598
  background:var(--panel);color:var(--ink);font:400 12.5px/1 var(--ui);padding:0 44px 0 12px;outline:none}
1582
1599
  #fsRoot .cc-ask .answ-inline input:focus{border-color:color-mix(in srgb,var(--blue) 55%,var(--hair2))}
1583
1600
  #fsRoot .cc-ask .answ-inline .sa-send{right:5px}
1584
- /* Delete (trash) stays hover-revealed, pinned top-right of the card (§1.5-10
1585
- icon buttons are never a permanent fixture); restores the「Needs your answer」
1586
- discard path. */
1587
- #fsRoot .cc-ask .acts{position:absolute;top:8px;right:8px;display:flex;opacity:0;transition:opacity .15s}
1588
- #fsRoot .cc-ask:hover .acts{opacity:1}
1589
- /* Touch devices have no hover → the hover-only trash was permanently invisible
1590
- and the「Needs your answer」card had no reachable discard path (Masa
1591
- 2026-07-16: 右上のdeleteが押せない). Reveal it (dimmed, so it still reads as
1592
- secondary per §1.5-10) whenever the pointer can't hover. */
1593
- @media (hover: none){ #fsRoot .cc-ask .acts{opacity:.55} }
1601
+ /* Which request this question belongs to, on the label's own line: "#835 モバイル
1602
+ でGalda is offline…". Not the mono/uppercase/blue of the label itself — that
1603
+ vocabulary means "this is a Question"; the goal is ordinary prose, and it is
1604
+ dim because it identifies rather than asks (§5.5 薄く・騒がない). */
1605
+ #fsRoot .cc-ask-of{font-family:var(--ui);font-size:11px;letter-spacing:0;text-transform:none;
1606
+ color:var(--ink3);margin-left:8px}
1607
+ /* Third question onward: a count, not a card (§1.5-8). A line of text, no box —
1608
+ the box is what would take the room this is trying to save. */
1609
+ #fsRoot .cc-ask-more{font-size:11px;color:var(--ink3);margin:4px 0 2px 2px}
1610
+ /* A trash used to sit here, hover-revealed top-right. It called fsTaskDelete —
1611
+ it deleted the GOAL — from a card that did not say which goal it was, next to
1612
+ the answer box (Masa 2026-07-29). Deleting lives on the card and in the right
1613
+ lane, where the thing being deleted is named. */
1594
1614
  #fsRoot .qchips{display:flex;gap:6px;flex-wrap:wrap}
1595
1615
  #fsRoot .achip{height:28px;padding:0 12px;border-radius:8px;border:1px solid var(--hair2);background:var(--panel);
1596
1616
  color:var(--ink2);font:inherit;font-size:12.5px;cursor:pointer;transition:border-color .15s,color .15s,background .15s}
@@ -1903,7 +1923,12 @@
1903
1923
  .sa-hd .keys{margin-left:auto;font:500 9px/1 var(--mono);color:var(--ink3);letter-spacing:.08em}
1904
1924
  .sa-x{width:30px;height:30px;border:0;border-radius:8px;background:transparent;color:var(--ink3);font-size:15px;cursor:pointer}
1905
1925
  .sa-x:hover{color:var(--ink);background:transparent}
1906
- .sa-body{flex:1;overflow-y:auto;overflow-x:hidden;padding:16px 22px 22px;scrollbar-width:none;display:flex;flex-direction:column;gap:12px;align-items:center}
1926
+ /* goal-806/#851 案3: 複数のレビュー(sa-item)を縦スクロールで読んでいるとき、
1927
+ どれが何件目かを左の細いレールで示す。sa-hd の "N / M" は明示アクション
1928
+ (↑↓/クリック/Approve)でしか動かないので、素のスクロールでは追従しない
1929
+ ——ここだけスクロール位置に連動してハイライトする。 */
1930
+ .sa-mid{flex:1;min-height:0;display:flex;align-items:stretch}
1931
+ .sa-body{flex:1;min-width:0;overflow-y:auto;overflow-x:hidden;padding:16px 22px 22px;scrollbar-width:none;display:flex;flex-direction:column;gap:12px;align-items:center}
1907
1932
  .sa-body::-webkit-scrollbar{display:none}
1908
1933
  .sa-item{background:var(--panel);border-radius:16px;box-shadow:0 1px 2px rgba(20,23,31,.05),0 0 0 1px var(--hair);padding:16px 18px;position:relative;width:min(720px,96%)}
1909
1934
  /* H14: single-review tap → the See-all card flashes a blue ring to guide the eye */
@@ -2192,6 +2217,14 @@
2192
2217
  .redesign .rshot:last-child{margin-bottom:0}
2193
2218
  .redesign .rshot img{display:block;width:100%;height:auto;border-radius:10px;border:1px solid var(--hair)}
2194
2219
  .redesign .rshot figcaption{margin-top:6px;font-family:var(--mono);font-size:11px;color:var(--ink3)}
2220
+ /* older shots from the same attempt fold away by default — the newest one already
2221
+ shows open; this is a manual "show more", not another auto-stacking pile. */
2222
+ .redesign .rshotfold{margin-top:8px}
2223
+ .redesign .rshotfold summary{cursor:pointer;font-family:var(--mono);font-size:11px;color:var(--ink3);list-style:none}
2224
+ .redesign .rshotfold summary::-webkit-details-marker{display:none}
2225
+ .redesign .rshotfold summary:before{content:'▸ ';color:var(--ink3)}
2226
+ .redesign .rshotfold[open] summary:before{content:'▾ '}
2227
+ .redesign .rshotfold .rshot{margin-top:12px}
2195
2228
  /* before/after pair + the red box over what actually changed (pixel-diffed here from
2196
2229
  the worker's own two shots — no box appears unless the pixels really differ) */
2197
2230
  .redesign .beforeafter .proofimg{width:100%;height:auto;max-height:none;object-fit:initial;display:block;border-radius:8px;border:1px solid var(--hair)}
@@ -2224,14 +2257,25 @@
2224
2257
  Replaces the single Request paragraph + separate "This review" section. */
2225
2258
  .redesign .rqlist{display:flex;flex-direction:column;gap:12px;margin-top:2px}
2226
2259
  .redesign .rqit{display:flex;gap:10px;align-items:baseline}
2227
- .redesign .rqit .rqn{flex:0 0 16px;font:600 12px/1.5 var(--mono);color:var(--ink3);font-variant-numeric:tabular-nums}
2260
+ .redesign .rqit .rqn{flex:0 0 16px;font:600 12px/1.5 var(--mono);color:var(--ink2);font-variant-numeric:tabular-nums}
2228
2261
  .redesign .rqit .rqbody{flex:1;min-width:0}
2229
2262
  /* §2 forces .rask to full-contrast ink !important everywhere else in the card (it's
2230
2263
  the user's own words, never faded) — the Request stack is the one place that rule
2231
2264
  is deliberately overridden: only the LATEST ask reads at full weight. */
2232
2265
  .redesign .rqit .rqbody .rbody.rask{color:var(--ink2) !important}
2233
- .redesign .rqit.latest .rqn{color:var(--blue,#7C9EFF)}
2234
- .redesign .rqit.latest .rqbody .rbody.rask{color:var(--ink) !important;font-weight:600}
2266
+ /* latest = monochrome (ink), not blue — a color for "current" competes with the review
2267
+ state dot/PR green elsewhere on the card (Masa 2026-07-29 follow-up). Weight B from
2268
+ the A/B compare (Masa picked B 2026-07-29): number stays ink2, body drops to 500 —
2269
+ calmer than the shipped bold ink. */
2270
+ .redesign .rqit.latest .rqn{color:var(--ink2)}
2271
+ .redesign .rqit.latest .rqbody .rbody.rask{color:var(--ink) !important;font-weight:500}
2272
+ /* Request stack cap (Masa 2026-07-29): middle asks collapse into one dots-only row,
2273
+ no label text, no "collapsing" wording — tap to expand in place. */
2274
+ .redesign .rqfold-row{margin:0;cursor:pointer}
2275
+ .redesign .rqfold-row>summary{list-style:none;display:flex;align-items:center;justify-content:center;padding:2px 0;border-radius:8px;font:600 13px/1 var(--mono);color:var(--ink3);letter-spacing:.08em}
2276
+ .redesign .rqfold-row>summary::-webkit-details-marker{display:none}
2277
+ .redesign .rqfold-row>summary:hover{background:var(--hover-s,rgba(255,255,255,.05));color:var(--ink)}
2278
+ .redesign .rqfold-row .rqmid{display:flex;flex-direction:column;gap:12px;margin-top:12px}
2235
2279
  /* Changed/Tests aligns with the request TEXT, not the number/chevron — same 18px
2236
2280
  the fold body below uses, so both sit under the same column (Masa 2026-07-29). */
2237
2281
  .redesign .rqit .rqev{font-size:12px;color:var(--ink3);margin:5px 0 0;padding-left:18px}
@@ -3280,6 +3324,8 @@
3280
3324
 
3281
3325
  <div class="overlay revfloat" id="reviewOverlay">
3282
3326
  <div class="modal reviewmodal">
3327
+ <div class="revrail" id="reviewRail"></div>
3328
+ <div class="revbody">
3283
3329
  <div class="revcount" id="reviewCount">REVIEW</div>
3284
3330
  <h3 id="reviewGoalTitle">Review</h3>
3285
3331
  <p id="reviewIntro">Review each requirement with its capture and approve it individually — approved ones fold away. Or approve the whole goal below to send it straight to Done; the next review pops in automatically. Tap outside to close.</p>
@@ -3305,6 +3351,7 @@
3305
3351
  <input type="text" id="reviewFixInput" placeholder="Type fix instructions (attach a screenshot too) — sends the goal back to the worker…">
3306
3352
  <button type="button" id="reviewFixSend">Send</button>
3307
3353
  </div>
3354
+ </div>
3308
3355
  </div>
3309
3356
  </div>
3310
3357
 
@@ -4187,6 +4234,20 @@ function saShotPair(shots, declared){
4187
4234
  const before = find(pair[0]), after = find(pair[1]);
4188
4235
  return before && after && before !== after ? { before, after } : null;
4189
4236
  }
4237
+ // A goal can carry several shots from one attempt (variant comparisons, retries). Piling
4238
+ // all of them full-width makes the Review dialog scroll forever (#881 follow-up), so only
4239
+ // the most recent shot shows open; older ones fold behind a disclosure the user opens on
4240
+ // demand — screenshots stop auto-stacking without losing any of them.
4241
+ function saShotFigHtml(sh){
4242
+ return `<figure class="rshot"><img src="${esc(sh.url)}" alt="${esc(sh.name || 'screenshot')}" loading="lazy"><figcaption>${esc(String(sh.name || '').replace(/\.[a-z]+$/i, ''))}</figcaption></figure>`;
4243
+ }
4244
+ function saShotsListHtml(shots){
4245
+ const list = shots ?? [];
4246
+ if (!list.length) return '';
4247
+ if (list.length === 1) return saShotFigHtml(list[0]);
4248
+ const older = list.slice(0, -1);
4249
+ return `${saShotFigHtml(list[list.length - 1])}<details class="rshotfold"><summary>ほかの${older.length}枚を表示</summary>${older.map(saShotFigHtml).join('')}</details>`;
4250
+ }
4190
4251
  function saPlainPairHtml(pair){
4191
4252
  const col = (sh, label) => `<div class="ba-col"><span class="balabel">${label}</span><img class="proofimg" src="${esc(sh.url)}" alt="${esc(label)}" loading="lazy"></div>`;
4192
4253
  return `<div class="beforeafter">${col(pair.before, 'Before')}${col(pair.after, 'After')}</div>`;
@@ -5459,6 +5520,7 @@ function openReviewChecklist(goalId){
5459
5520
  }
5460
5521
  function closeReviewChecklist(){
5461
5522
  $('reviewOverlay').classList.remove('show');
5523
+ reviewRailWiring?.destroy(); reviewRailWiring = null;
5462
5524
  const goalId = state.reviewOpenGoal;
5463
5525
  state.reviewOpenGoal = null;
5464
5526
  // task 86: レビュー画面からチャットへ戻る瞬間に、そのゴールの末尾(完了して
@@ -5625,6 +5687,7 @@ function renderReviewChecklist(){
5625
5687
  ${chat}` : ''}
5626
5688
  </div>`;
5627
5689
  }).join('');
5690
+ renderReviewRail(reqs);
5628
5691
  for (const b of $('reviewList').querySelectorAll('.reqbtn')) {
5629
5692
  b.onclick = () => setRequirementStatus(goalId, Number(b.dataset.task), b.dataset.act);
5630
5693
  }
@@ -5659,6 +5722,56 @@ function renderReviewChecklist(){
5659
5722
  };
5660
5723
  }
5661
5724
  }
5725
+ // goal-806/#851 案3: 複数カードのレビューで「今どこを見ているか」を左の細い
5726
+ // レールの番号で示す共通ヘルパー。scroller をスクロールするたび、その上端
5727
+ // から少し(bandPx)下のラインを跨いでいるカードを「今読んでいる」と判定して
5728
+ // ハイライトする(IntersectionObserver の rootMargin では、まだ画面外にある
5729
+ // 手前のカードの巨大な rect が「一部交差」して誤って選ばれることがあるため、
5730
+ // 素直に座標で判定する)。クリックは scrollIntoView(smooth) で気持ちよく
5731
+ // その見出し頭へ着地させる。戻り値は teardown 用の { destroy() }。
5732
+ function wireScrollRail({ rail, scroller, cards, getKey, onClick }){
5733
+ if (!rail || !scroller || cards.length < 2) { rail?.classList.remove('show'); if (rail) rail.innerHTML = ''; return { destroy(){} }; }
5734
+ rail.classList.add('show');
5735
+ const setOn = (key) => { for (const b of rail.querySelectorAll('button')) b.classList.toggle('on', b.dataset.railkey === String(key)); };
5736
+ setOn(getKey(cards[0]));
5737
+ for (const b of rail.querySelectorAll('button')) {
5738
+ b.onclick = () => onClick(b.dataset.railkey);
5739
+ }
5740
+ // task 959/960: bandPx=24 だと「タイトルが天井にほぼ着くまで」次カードに切り替わらず、
5741
+ // ユーザーが実際に読んでいる体感より遅れる。960でさらに早めてほしいとの指摘があり、
5742
+ // scroller高さの1/2程度(下限40px・上限260px)を判定ラインにして、少しスクロールした
5743
+ // だけで次のカードへ移った体感になるようにする。scroller.clientHeight はダイアログの
5744
+ // レイアウト完了後でないと0のことがあるので、固定値でなく毎回 onScroll 内で読み直す。
5745
+ const onScroll = () => {
5746
+ const bandPx = Math.min(260, Math.max(40, scroller.clientHeight * 0.5));
5747
+ const bandY = scroller.getBoundingClientRect().top + bandPx;
5748
+ let current = cards[0];
5749
+ for (const card of cards) {
5750
+ const r = card.getBoundingClientRect();
5751
+ if (r.top <= bandY) current = card; else break;
5752
+ }
5753
+ setOn(getKey(current));
5754
+ };
5755
+ scroller.addEventListener('scroll', onScroll, { passive: true });
5756
+ onScroll();
5757
+ return { destroy(){ scroller.removeEventListener('scroll', onScroll); } };
5758
+ }
5759
+ let reviewRailWiring = null;
5760
+ function renderReviewRail(reqs){
5761
+ const rail = $('reviewRail');
5762
+ if (!rail) return;
5763
+ reviewRailWiring?.destroy();
5764
+ if (reqs.length < 2) { rail.classList.remove('show'); rail.innerHTML = ''; return; }
5765
+ rail.classList.add('show');
5766
+ rail.innerHTML = reqs.map((t, i) => `<button type="button" data-railkey="${t.id}" title="${esc(t.title ?? '')}"><span class="bar"></span>${t.num ?? i + 1}</button>`).join('');
5767
+ const scroller = $('reviewList').closest('.reviewmodal');
5768
+ const cards = [...$('reviewList').querySelectorAll('.reqcard')];
5769
+ reviewRailWiring = wireScrollRail({
5770
+ rail, scroller, cards,
5771
+ getKey: (card) => card.dataset.task ?? card.dataset.reopen,
5772
+ onClick: (key) => cards.find((c) => (c.dataset.task ?? c.dataset.reopen) === key)?.scrollIntoView({ behavior: 'smooth', block: 'start' }),
5773
+ });
5774
+ }
5662
5775
  // task 122: render()で消えるReviewの行を、Doneの行位置へ『飛んでいく』ように
5663
5776
  // 見せるFLIPアニメーション。captureReviewFly()をrender()の直前(行がまだ
5664
5777
  // DOMにある間)に呼んで元位置を覚え、playReviewFly()をrender()の直後に呼んで
@@ -6779,17 +6892,38 @@ function renderFsFeed(){
6779
6892
  // Questions render at the FEED BOTTOM (composer-adjacent) — built into askHtml
6780
6893
  // so they sit AFTER the conversation turns (H18/H19 / Pattern A: 質問は末尾固定).
6781
6894
  let askHtml = '';
6782
- for (const g of state.goals.filter((g) => g.projectId === state.active && g.status === 'needsInput' && g.question)) {
6895
+ // This loop renders EVERY goal in the project that is waiting on an answer
6896
+ // not the one you happen to be watching — and the card used to print nothing
6897
+ // but the word "Question" and the text. Two of them stacked, each with its own
6898
+ // free-text box, and nothing on screen said which request you were answering
6899
+ // (Masa 2026-07-29, with #835 and #856 sitting inside goal-847's live feed).
6900
+ // The right lane has named them all along ("#835 モバイルでGalda is offline…");
6901
+ // this makes the middle say the same thing, in the same shape.
6902
+ const asking = state.goals.filter((g) => g.projectId === state.active && g.status === 'needsInput' && g.question);
6903
+ // §1.5-8 量には畳みで先に応え、スクロールは後. Past the second card the rest are
6904
+ // counted, not drawn — a queue of questions must not push the live output off
6905
+ // the screen. They are all still in Needs you, which is where the count points.
6906
+ const ASK_SHOWN = 2;
6907
+ for (const g of asking.slice(0, ASK_SHOWN)) {
6783
6908
  const chips = (g.question.options || []).map((o) => `<button class="achip" data-fsanswer="${g.id}" data-a="${esc(o)}">${esc(o)}</button>`).join('');
6784
- // QUESTION card (H18): mono label + prompt + option chips + always-on free-text
6785
- // answer + hover trash. Data hooks (data-fsanswer / -ansinput / -anssend /
6786
- // -answerdel) are unchanged so the existing wiring below keeps working.
6787
- askHtml += `<div class="cc-ask"><div class="cc-ask-lb">Question</div>`
6909
+ const title = String(g.text ?? '').replace(/\s+/g, ' ').trim().slice(0, 60);
6910
+ // QUESTION card (H18): mono label + the goal it belongs to + prompt + option
6911
+ // chips + always-on free-text answer. Data hooks (data-fsanswer / -ansinput /
6912
+ // -anssend) are unchanged so the existing wiring below keeps working.
6913
+ //
6914
+ // The trash is deliberately absent (Masa 2026-07-29). It called fsTaskDelete:
6915
+ // it deleted the GOAL, not the question, from an unlabelled card — and an
6916
+ // irreversible action does not belong beside a text box in a feed you are
6917
+ // skim-reading. Deleting still lives on the card and in the right lane.
6918
+ askHtml += `<div class="cc-ask"><div class="cc-ask-lb">Question`
6919
+ + `<span class="cc-ask-of">#${esc(String(g.id))}${title ? ` ${esc(title)}` : ''}</span></div>`
6788
6920
  + `<div class="cc-ask-q">${esc(g.question.text)}</div>`
6789
6921
  + (chips ? `<div class="qchips">${chips}</div>` : '')
6790
6922
  + `<div class="answ-inline"><input type="text" data-fsansinput="${g.id}" placeholder="Type your answer…" aria-label="Type your answer">`
6791
- + `<button type="button" class="sa-send" data-fsanssend="${g.id}" title="Send answer" aria-label="Send answer"><svg viewBox="0 0 24 24"><path d="M12 19V5M5 12l7-7 7 7"/></svg></button></div>`
6792
- + `<span class="acts"><button class="ab" data-fsanswerdel="${g.id}" title="Delete">${FS_ICONS.del}</button></span></div>`;
6923
+ + `<button type="button" class="sa-send" data-fsanssend="${g.id}" title="Send answer" aria-label="Send answer"><svg viewBox="0 0 24 24"><path d="M12 19V5M5 12l7-7 7 7"/></svg></button></div></div>`;
6924
+ }
6925
+ if (asking.length > ASK_SHOWN) {
6926
+ askHtml += `<div class="cc-ask-more">+${asking.length - ASK_SHOWN} more waiting on you</div>`;
6793
6927
  }
6794
6928
  // Clawd states (§4.5, driven by REAL state — no timers): activity in this column
6795
6929
  // → speaker row (Clawd + Claude Code + goal metadata) above the raw CC lines;
@@ -6874,7 +7008,7 @@ function renderFsFeed(){
6874
7008
  };
6875
7009
  // Delete a "needs your answer" card outright (restores 「Needs your answer」カードを削除
6876
7010
  // できるように — a question you don't want to answer had no discard path in flagship).
6877
- for (const b of feed.querySelectorAll('[data-fsanswerdel]')) b.onclick = (e) => { e.stopPropagation(); fsTaskDelete(Number(b.dataset.fsanswerdel)); };
7011
+ // data-fsanswerdel is gone with the trash it wired (see .cc-ask above).
6878
7012
  }
6879
7013
  async function fsAnswerQuestion(goalId, answer){
6880
7014
  if (!answer) return;
@@ -9422,6 +9556,8 @@ function closeSummaryPera(){ $('summaryOverlay').classList.remove('show'); }
9422
9556
  const SA = { open: false, items: [], diffs: {}, actLog: {}, msgs: {}, convOpen: new Set(), shotBox: {}, detOpen: new Set(), diffOpen: new Set(), attachments: {}, focusGid: null, focusScrolled: false, focusTimer: null, cur: null, solo: null, lastMarkup: null };
9423
9557
  // Same convention as the existing ?dev=1 devbar flag — off unless someone opts in by URL.
9424
9558
  const SA_DEV = new URLSearchParams(location.search).get('dev') === '1';
9559
+ // task-971: 白黒(モノクロ)版のレールを ?railmono=1 で見比べられるようにする。
9560
+ const SA_RAIL_MONO = new URLSearchParams(location.search).get('railmono') === '1';
9425
9561
  function saReplyAttachmentsHtml(goalId){
9426
9562
  return (SA.attachments[goalId] || []).map((a, i) => `<span class="sa-replythumb"><img src="${esc(a.url)}" alt="Attached image"><button type="button" data-saarm="${goalId}:${i}" aria-label="Remove image">×</button></span>`).join('');
9427
9563
  }
@@ -9983,7 +10119,7 @@ function saReqStackRows(it, attemptEvidence){
9983
10119
  const reqMsgs = SA.msgs[gk];
9984
10120
  const reqAsks = Array.isArray(reqMsgs) ? reqMsgs.filter((m) => m?.from === 'you' && String(m.text || '').trim()) : [];
9985
10121
  const reqItems = reqAsks.length ? reqAsks : (it.goalText ? [{ text: it.goalText }] : []);
9986
- return reqItems.map((m, idx) => {
10122
+ const row = (m, idx) => {
9987
10123
  const text = String(m.text || '').trim();
9988
10124
  const isLatest = idx === reqItems.length - 1;
9989
10125
  const evLine = isLatest && attemptEvidence ? `<p class="rqev">${esc(attemptEvidence)}</p>` : '';
@@ -9997,7 +10133,19 @@ function saReqStackRows(it, attemptEvidence){
9997
10133
  })()
9998
10134
  : `<p class="rbody rask">${esc(text)}</p>`;
9999
10135
  return `<div class="rqit${isLatest ? ' latest' : ''}"><span class="rqn">${idx + 1}</span><div class="rqbody">${body}${evLine}</div></div>`;
10000
- }).join('');
10136
+ };
10137
+ // Cap the stack at first + latest 2 (Masa 2026-07-29 follow-up: "3個以上の表示はいらない
10138
+ // ...良い感じに間を...にして"). The first ask (original request) and the latest 2 (current
10139
+ // state) stay visible; anything in between collapses into one dots-only row (no label
10140
+ // text, no "collapsing" wording) that expands in place — numbering stays the real,
10141
+ // continuous index so nothing reads as renumbered.
10142
+ if (reqItems.length <= 3) return reqItems.map(row).join('');
10143
+ const midFrom = 1, midTo = reqItems.length - 3;
10144
+ const foldKey = `${gk}:reqfold`;
10145
+ const midOpen = SA.detOpen.has(foldKey);
10146
+ const midRows = reqItems.slice(midFrom, midTo + 1).map((m, k) => row(m, midFrom + k)).join('');
10147
+ const foldRow = `<details class="rqfold-row" data-detkey="${foldKey}"${midOpen ? ' open' : ''}><summary class="rqdots">···</summary><div class="rqmid">${midRows}</div></details>`;
10148
+ return row(reqItems[0], 0) + foldRow + reqItems.slice(-2).map((m, k) => row(m, reqItems.length - 2 + k)).join('');
10001
10149
  }
10002
10150
  function saItemHtml(it, i){
10003
10151
  // st===3 is NOT a terminal row (REVIEW-FEEDBACK-FLOW 決定3): a reply is not a dismissal.
@@ -10118,6 +10266,15 @@ function saItemHtml(it, i){
10118
10266
  const reqSec = reqStackRows
10119
10267
  ? `<section class="rsec rreq" data-reqmsgs="${gk}"><div class="rsec-h">Request</div><div class="rqlist">${reqStackRows}</div></section>`
10120
10268
  : '';
10269
+ // ── Summary (Masa 2026-07-29, 案2): "何の修正か一目で分からない" への対策として、雑に
10270
+ //書かれたRequest本文の上に一言サマリを置く。色は使わない — 他のセクションと同じ
10271
+ // rsec/rsec-h の見た目に揃えた通常セクション(案1のバナー・案3の絵文字強調は却下)。
10272
+ // 出典はレビュー要約(reviewSummary.changed)のみ — worker側の新フィールドを増やさず、
10273
+ // 既存の値をそのまま短く見せるだけなのでworker(Claude Code/Codex)側の負担は増えない。
10274
+ const summaryText = String(it.goal?.reviewSummary?.changed || '').replace(/\s+/g, ' ').trim();
10275
+ const summarySec = summaryText
10276
+ ? `<section class="rsec rsum"><div class="rsec-h">Summary</div><p class="rbody v">${esc(summaryText.slice(0, 200))}</p></section>`
10277
+ : '';
10121
10278
  const coverage = (it.requirementCoverage || []).filter((r) => r.text);
10122
10279
  const coverageRows = coverage.map((r) => {
10123
10280
  const labels = { verified: 'Verified', failed: 'Failed', 'not-applicable': 'Not applicable', unverified: 'Not verified' };
@@ -10238,9 +10395,7 @@ function saItemHtml(it, i){
10238
10395
  const boxed = shotPair ? SA.shotBox[it.goalId] : null;
10239
10396
  const shotsBody = shotPair
10240
10397
  ? `<div class="rshotpair" data-shotpair="${it.goalId}">${typeof boxed === 'string' ? boxed : saPlainPairHtml(shotPair)}</div>`
10241
- : it.shots?.length
10242
- ? it.shots.map((sh) => `<figure class="rshot"><img src="${esc(sh.url)}" alt="${esc(sh.name || 'screenshot')}" loading="lazy"><figcaption>${esc(String(sh.name || '').replace(/\.[a-z]+$/i, ''))}</figcaption></figure>`).join('')
10243
- : '';
10398
+ : saShotsListHtml(it.shots);
10244
10399
  // ── Proof (spec §6/§11): before/after (identical size) when a real image exists; a
10245
10400
  // run-log for logic-only tests; dropped entirely for a conversational task. ──
10246
10401
  let proofInner = '';
@@ -10375,6 +10530,7 @@ function saItemHtml(it, i){
10375
10530
  <div class="rhead"><span class="rtag">${it.no != null ? `#${it.no}` : ''}</span>${rst}</div>
10376
10531
  <h2 class="rtitle">${esc(it.title)}</h2>
10377
10532
  ${meta}
10533
+ ${summarySec}
10378
10534
  ${reqSec}
10379
10535
  ${S('Open it', openBody + infraBody)}
10380
10536
  ${S('What to check', checkBody)}
@@ -10482,6 +10638,25 @@ async function saEnhanceShotBoxes(){
10482
10638
  if (host) host.innerHTML = html;
10483
10639
  }));
10484
10640
  }
10641
+ let saRailWiring = null;
10642
+ // goal-806/#851 案3: 素のスクロールでは SA.cur/"N / M" が動かないので、レール
10643
+ // だけスクロール位置を追いかけて示す(SA.cur 自体は触らない=↑↓/Approve の
10644
+ // 対象を変えない)。共通実装は wireScrollRail (engine 側の renderReviewRail と同じ)。
10645
+ function renderSaRail(pending, body){
10646
+ const rail = $('saRail');
10647
+ if (!rail) return;
10648
+ saRailWiring?.destroy(); saRailWiring = null;
10649
+ if (!body || pending.length < 2) { rail.classList.remove('show'); rail.innerHTML = ''; return; }
10650
+ rail.classList.add('show');
10651
+ rail.classList.toggle('mono', SA_RAIL_MONO);
10652
+ rail.innerHTML = pending.map((it, i) => `<button type="button" data-railkey="${it.goalId}" title="${esc((it.title ?? '').slice(0, 80))}"><span class="bar"></span>${i + 1}</button>`).join('');
10653
+ const cards = [...body.querySelectorAll('.sa-item[data-gid]')];
10654
+ saRailWiring = wireScrollRail({
10655
+ rail, scroller: body, cards,
10656
+ getKey: (card) => card.dataset.gid,
10657
+ onClick: (key) => body.querySelector(`.sa-item[data-gid="${key}"]`)?.scrollIntoView({ behavior: 'smooth', block: 'start' }),
10658
+ });
10659
+ }
10485
10660
  function saRender(){
10486
10661
  const S = $('seeall'); if (!S) return;
10487
10662
  // SSE, diff fetches and action responses rebuild the dialog. Remember the
@@ -10536,7 +10711,8 @@ function saRender(){
10536
10711
  // a synthetic "new review just arrived" render the same way saSync does mid-read.
10537
10712
  const devInjectBtn = SA_DEV ? '<button class="sa-x" data-saact="devinject:0" title="Insert a fake review above the current scroll (dev only)">+TEST</button>' : '';
10538
10713
  const markup = `<div class="sa-panel"><div class="sa-hd"><span class="t">${hdTitle}</span><span class="c">${c}</span><span class="sa-count">${posLabel}</span><span class="keys">${hdKeys}</span>${devInjectBtn}<button class="sa-x" data-saact="close:0">✕</button></div>
10539
- <div class="sa-body">${SA.items.length ? SA.items.map((it, i) => saItemHtml(it, i)).join('') : '<div class="sa-empty">Nothing waiting for review.</div>'}</div></div>`;
10714
+ <div class="sa-mid"><div class="revrail" id="saRail"></div>
10715
+ <div class="sa-body">${SA.items.length ? SA.items.map((it, i) => saItemHtml(it, i)).join('') : '<div class="sa-empty">Nothing waiting for review.</div>'}</div></div></div>`;
10540
10716
  // Most SSE goal/task events are unrelated to the card the person is reading.
10541
10717
  // Replacing #seeall with identical markup destroyed and recreated every node,
10542
10718
  // producing a blank flash and resetting browser-owned state. Keep the current
@@ -10560,6 +10736,7 @@ function saRender(){
10560
10736
  SA.lastMarkup = markup;
10561
10737
  S.innerHTML = markup;
10562
10738
  const body = S.querySelector('.sa-body');
10739
+ renderSaRail(pending, body);
10563
10740
  let focusPlacedNow = false;
10564
10741
  // H14's "scroll the focused card to top" used to always run one requestAnimationFrame
10565
10742
  // after this point. That's still required for the very FIRST render of a fresh
@@ -10731,7 +10908,7 @@ function saOpen(focusGoalId, solo = false){
10731
10908
  saFetchDiffs();
10732
10909
  saEnhanceShotBoxes();
10733
10910
  }
10734
- function saClose(){ SA.open = false; SA.solo = null; SA.focusGid = null; SA.lastMarkup = null; if (SA.focusTimer) { clearTimeout(SA.focusTimer); SA.focusTimer = null; } $('seeall').classList.remove('open'); applyPendingUiVersionIfAny(); }
10911
+ function saClose(){ SA.open = false; SA.solo = null; SA.focusGid = null; SA.lastMarkup = null; if (SA.focusTimer) { clearTimeout(SA.focusTimer); SA.focusTimer = null; } saRailWiring?.destroy(); saRailWiring = null; $('seeall').classList.remove('open'); applyPendingUiVersionIfAny(); }
10735
10912
  // Continuous review (Masa 2026-07-08): move a persistent "current" card with
10736
10913
  // ↑↓ and auto-advance to the next pending after each verdict — same screen, same
10737
10914
  // Ledger, so approving flows without hunting for the next one.
package/engine/lib.mjs CHANGED
@@ -3609,6 +3609,44 @@ export function pickChromePath(env = {}, exists = () => false) {
3609
3609
  return chromeCandidates(env).find((c) => exists(c)) ?? null;
3610
3610
  }
3611
3611
 
3612
+ // What THIS machine can and cannot do, stated before the person spends an hour
3613
+ // reading the absence as a product bug. Two capabilities are host-dependent and
3614
+ // silently degrade today: without Chrome nothing can take a screenshot or verify
3615
+ // in a browser, and without a native picker "Open folder…" cannot open Finder.
3616
+ //
3617
+ // Every check here is ADVISORY. None of them stop anything — the app works with
3618
+ // neither. That is deliberately NOT testEnvironmentPreflight's blocking/canRun
3619
+ // model (which answers "can `npm test` even start"): mixing the two is what made
3620
+ // a missing puppeteer read as a failed verification.
3621
+ // (`existsFn`/`env` are injected so this stays a pure function under test.)
3622
+ export function hostReadiness({ platform = 'darwin', env = {}, existsFn = () => false } = {}) {
3623
+ const chrome = pickChromePath(env, existsFn);
3624
+ const onPath = (cmd) => isCommandOnPath(cmd, {
3625
+ pathEnv: env.PATH ?? '', sep: platform === 'win32' ? ';' : ':', pathExt: env.PATHEXT ?? '', existsFn,
3626
+ });
3627
+ // macOS and Windows ship theirs; Linux has two common ones and neither is a given.
3628
+ const picker = platform === 'darwin' ? 'osascript'
3629
+ : platform === 'win32' ? 'powershell.exe'
3630
+ : (onPath('zenity') ? 'zenity' : 'kdialog');
3631
+ const hasPicker = onPath(picker);
3632
+ return [
3633
+ {
3634
+ id: 'chrome',
3635
+ ok: Boolean(chrome),
3636
+ detail: chrome
3637
+ ? `Chrome/Chromium: ${chrome}`
3638
+ : 'No Chrome/Chromium found — screenshots and browser checks are unavailable',
3639
+ },
3640
+ {
3641
+ id: 'folder-picker',
3642
+ ok: hasPicker,
3643
+ detail: hasPicker
3644
+ ? `Native folder picker: ${picker}`
3645
+ : `No native folder picker (${picker} is not installed) — Open folder… asks you to type the path`,
3646
+ },
3647
+ ];
3648
+ }
3649
+
3612
3650
  // `node <manager>/engine/shot.mjs <url|file> <out.png> [--width N] [--height N]
3613
3651
  // [--full] [--wait MS] [--wait-for SELECTOR] [--element SELECTOR] [--clip x,y,w,h]`
3614
3652
  // Returns { error } instead of throwing, so the CLI can print one clear line.
package/engine/server.mjs CHANGED
@@ -20,7 +20,7 @@ import { resolve, dirname, join, basename } from 'node:path';
20
20
  import { homedir } from 'node:os';
21
21
  import { fileURLToPath } from 'node:url';
22
22
  import { pathToFileURL } from 'node:url';
23
- import { needsProjectFolder, folderLabel, chooseFolderScript, parseChosenFolder, buildFolderQuestion, resolveFolderAnswer, classifyFolderTarget, buildFolderInitConfirm, isFolderInitConfirmed, needsReviewPreference, buildReviewPreferenceQuestion, resolveReviewPreferenceAnswer, routeQuestionAnswer, notUnderstoodNote, parseRelocalizedQuestion, classifyAuthProbe, authBannerText, makeSigninChallenge, parseClaimResponse, isCommandOnPath, parseRequestedWorkspaceDir } from './lib.mjs';
23
+ import { needsProjectFolder, folderLabel, chooseFolderScript, parseChosenFolder, buildFolderQuestion, resolveFolderAnswer, classifyFolderTarget, buildFolderInitConfirm, isFolderInitConfirmed, needsReviewPreference, buildReviewPreferenceQuestion, resolveReviewPreferenceAnswer, routeQuestionAnswer, notUnderstoodNote, parseRelocalizedQuestion, classifyAuthProbe, authBannerText, makeSigninChallenge, parseClaimResponse, isCommandOnPath, hostReadiness, parseRequestedWorkspaceDir } from './lib.mjs';
24
24
  import { parseStreamEvents, parseCodexEvents, buildCodexArgs, parsePlan, refinePlanTasks, canEditGoal, canDeleteGoal, shouldAskClarification, workerExitReason, classifyWorkerFailure, isForcedStop, taskStatusAfterVerify, workerResultText, taskCountsAsComplete, resolveWantsPR, resolveGoalSource, buildGoalSummary, buildReviewAttemptLedger, latestGoalOutcomeTask, buildDirtyWorkspacePrBlock, buildGoalProofMd, buildGoalPrBody, buildReviewSummaryPrompt, parseReviewSummary, buildReviewRuleSection, nextGoalStatus, isPrMerged, isPrApproved, reviewToDoneStatus, advanceReviewGoal, revertReviewGoal, approveGoal, dismissGoal, rejectGoal, reopenGoal, permissionModeFor, isPlanReview, nextAfterPlanApprove, GOAL_MODES, parseSkillFrontmatter, collectSkills, retestGoal, cancelRetestGoal, computeRetestOutcome, revertGoal, planRevertActions, archiveGoal, unarchiveGoal, undismissGoal, parseNumstat, truncateDiffText, sumUsage, resolveEntryUrl, rebasePreviewUrl, shouldCreateGoalPR, normalizePullRequestUrl, parseGitLog, diffNewCommits, replayQueueLog, reconcileOrphanGoals, reorderQueue, sortQueueByPriority, TASK_PRIORITIES, validateWorkflowColumns, DEFAULT_WORKFLOW_COLUMNS, validateReviewDefinition, DEFAULT_REVIEW_DEFINITION, resolveTestGate, buildEphemeralSeedLog, buildEphemeralSeedProjects, trimTaskActivityForState, buildAskContext, WORKER_TOOLS, workerPrompt, verifyCfAccessJwt, resolveIdentity, goalVisibleTo, clampParallelLimit, canStartMore, 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';
@@ -1726,8 +1726,26 @@ function activeSlotCount(projectId = null) {
1726
1726
  if (projectId != null) return (queues.get(projectId)?.running.size ?? 0) + planningCount(projectId);
1727
1727
  return globalRunningCount() + planningCount();
1728
1728
  }
1729
+ // A goal holds its slot for as long as it is in Doing — from the moment planning
1730
+ // starts until it leaves — and NOT only while one of its tasks happens to be
1731
+ // executing.
1732
+ //
1733
+ // activeSlotCount() counts running TASKS (plus goals still planning). That is the
1734
+ // right question for pump(), which decides how many workers may run. It is the
1735
+ // wrong question here: a goal whose task was sitting in the queue counted for
1736
+ // nothing, so a free slot appeared, so the next stacked goal was planned too —
1737
+ // and so on. Measured with MANAGER_MAX_PARALLEL=2 and four goals: two tasks ran
1738
+ // (the cap held), while all four goals sat in Doing and `stacked` never had a
1739
+ // single member. The board says four things are being worked on when two are,
1740
+ // and because nothing is `stacked`, the row that explains the wait — "Waiting
1741
+ // for parallel slot · active #N · starts when one reaches Review/Done" — never
1742
+ // renders at all.
1743
+ function activeGoalSlots(projectId = null) {
1744
+ return goals.filter((g) => (g.status === 'planning' || g.status === 'running')
1745
+ && (projectId == null || g.projectId === projectId)).length;
1746
+ }
1729
1747
  function stackedStartSlots(projectId) {
1730
- return Math.min(projectParallelCap() - activeSlotCount(projectId), GLOBAL_MAX_PARALLEL - activeSlotCount());
1748
+ return Math.min(projectParallelCap() - activeGoalSlots(projectId), GLOBAL_MAX_PARALLEL - activeGoalSlots());
1731
1749
  }
1732
1750
  function startPlanningGoal(goal) {
1733
1751
  goal.status = 'planning';
@@ -3317,6 +3335,14 @@ function testNodePath(dir, dependencySourceDir = null) {
3317
3335
  .filter(Boolean).filter(existsSync);
3318
3336
  return [...new Set([...local, ...(process.env.NODE_PATH ? process.env.NODE_PATH.split(sep) : [])])].join(sep);
3319
3337
  }
3338
+ // Advisory host capabilities for /api/state (see hostReadiness in lib.mjs). This
3339
+ // touches the filesystem, and /api/state is polled every couple of seconds, so
3340
+ // answer once: installing Chrome mid-session is not a thing to poll for, and a
3341
+ // restart is already how this app picks up a new environment.
3342
+ let _hostReadiness = null;
3343
+ function hostReadinessNow() {
3344
+ return (_hostReadiness ??= hostReadiness({ platform: process.platform, env: process.env, existsFn: existsSync }));
3345
+ }
3320
3346
  function testEnvironmentPreflight(dir, dependencySourceDir = null) {
3321
3347
  const nodePath = testNodePath(dir, dependencySourceDir);
3322
3348
  const sources = nodePath ? nodePath.split(process.platform === 'win32' ? ';' : ':') : [];
@@ -4349,7 +4375,7 @@ const server = createServer(async (req, res) => {
4349
4375
  const dirty = projectDirtyInfo(p.dir);
4350
4376
  return { ...p, ...(branch ? { branch } : {}), ...dirty };
4351
4377
  });
4352
- return json(res, 200, { projects: projectsWithBranch, goals: visibleGoals, tasks: trimTaskActivityForState(visibleTasks), models: MODELS, agentModels: { 'claude-code': MODELS, codex: CODEX_MODELS }, agentEfforts: { 'claude-code': CLAUDE_EFFORTS, codex: CODEX_EFFORTS }, agents: AVAILABLE_AGENTS, connectCommand, queueOrder, externalActivity, workflowColumns: projectColumns, reviewDefinitions: projectReviewDefs, runningCounts, parallelLimits, uiVersion: uiVersion(), billing, auth: publicAuth() });
4378
+ return json(res, 200, { projects: projectsWithBranch, goals: visibleGoals, tasks: trimTaskActivityForState(visibleTasks), models: MODELS, agentModels: { 'claude-code': MODELS, codex: CODEX_MODELS }, agentEfforts: { 'claude-code': CLAUDE_EFFORTS, codex: CODEX_EFFORTS }, agents: AVAILABLE_AGENTS, connectCommand, queueOrder, externalActivity, workflowColumns: projectColumns, reviewDefinitions: projectReviewDefs, runningCounts, parallelLimits, uiVersion: uiVersion(), billing, hostReadiness: hostReadinessNow(), auth: publicAuth() });
4353
4379
  }
4354
4380
 
4355
4381
  // Read-only stability snapshot: separates implementation failures from
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@galda/cli",
3
- "version": "0.10.105",
3
+ "version": "0.10.107",
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": {