@galda/cli 0.10.104 → 0.10.106

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,17 +2257,41 @@
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}
2282
+ /* Canonical requirement coverage: one compact ledger, not three competing
2283
+ interpretations. Status is text + a small rule/mark (no badge surface),
2284
+ and Manager checks stay visually distinct from worker claims. */
2285
+ .redesign .rqcov{display:flex;flex-direction:column;margin-top:2px}
2286
+ .redesign .rqcov-row{padding:11px 0;border-top:1px solid var(--hair)}
2287
+ .redesign .rqcov-row:first-child{padding-top:0;border-top:0}
2288
+ .redesign .rqcov-main{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:14px;align-items:baseline}
2289
+ .redesign .rqcov-text{font-size:13.5px;line-height:1.55;color:var(--ink)}
2290
+ .redesign .rqcov-state{font:600 10px/1.4 var(--mono);letter-spacing:.02em;color:var(--ink3);white-space:nowrap}
2291
+ .redesign .rqcov-state.verified{color:var(--green)}
2292
+ .redesign .rqcov-state.failed{color:var(--danger)}
2293
+ .redesign .rqcov-detail{margin-top:5px;font-size:11.5px;line-height:1.55;color:var(--ink3)}
2294
+ .redesign .rqcov-detail b{font-weight:600;color:var(--ink2)}
2238
2295
  /* A long ask folds with the SAME .lacc pulldown Activity/Conversation use — chevron on
2239
2296
  the peek line, body is the genuine rest (no duplicate, no trailing "…": Masa
2240
2297
  2026-07-29 flagged both the vertical anchor line and the "…" as unnecessary — the
@@ -3267,6 +3324,8 @@
3267
3324
 
3268
3325
  <div class="overlay revfloat" id="reviewOverlay">
3269
3326
  <div class="modal reviewmodal">
3327
+ <div class="revrail" id="reviewRail"></div>
3328
+ <div class="revbody">
3270
3329
  <div class="revcount" id="reviewCount">REVIEW</div>
3271
3330
  <h3 id="reviewGoalTitle">Review</h3>
3272
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>
@@ -3292,6 +3351,7 @@
3292
3351
  <input type="text" id="reviewFixInput" placeholder="Type fix instructions (attach a screenshot too) — sends the goal back to the worker…">
3293
3352
  <button type="button" id="reviewFixSend">Send</button>
3294
3353
  </div>
3354
+ </div>
3295
3355
  </div>
3296
3356
  </div>
3297
3357
 
@@ -4174,6 +4234,20 @@ function saShotPair(shots, declared){
4174
4234
  const before = find(pair[0]), after = find(pair[1]);
4175
4235
  return before && after && before !== after ? { before, after } : null;
4176
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
+ }
4177
4251
  function saPlainPairHtml(pair){
4178
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>`;
4179
4253
  return `<div class="beforeafter">${col(pair.before, 'Before')}${col(pair.after, 'After')}</div>`;
@@ -5446,6 +5520,7 @@ function openReviewChecklist(goalId){
5446
5520
  }
5447
5521
  function closeReviewChecklist(){
5448
5522
  $('reviewOverlay').classList.remove('show');
5523
+ reviewRailWiring?.destroy(); reviewRailWiring = null;
5449
5524
  const goalId = state.reviewOpenGoal;
5450
5525
  state.reviewOpenGoal = null;
5451
5526
  // task 86: レビュー画面からチャットへ戻る瞬間に、そのゴールの末尾(完了して
@@ -5612,6 +5687,7 @@ function renderReviewChecklist(){
5612
5687
  ${chat}` : ''}
5613
5688
  </div>`;
5614
5689
  }).join('');
5690
+ renderReviewRail(reqs);
5615
5691
  for (const b of $('reviewList').querySelectorAll('.reqbtn')) {
5616
5692
  b.onclick = () => setRequirementStatus(goalId, Number(b.dataset.task), b.dataset.act);
5617
5693
  }
@@ -5646,6 +5722,56 @@ function renderReviewChecklist(){
5646
5722
  };
5647
5723
  }
5648
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
+ }
5649
5775
  // task 122: render()で消えるReviewの行を、Doneの行位置へ『飛んでいく』ように
5650
5776
  // 見せるFLIPアニメーション。captureReviewFly()をrender()の直前(行がまだ
5651
5777
  // DOMにある間)に呼んで元位置を覚え、playReviewFly()をrender()の直後に呼んで
@@ -6766,17 +6892,38 @@ function renderFsFeed(){
6766
6892
  // Questions render at the FEED BOTTOM (composer-adjacent) — built into askHtml
6767
6893
  // so they sit AFTER the conversation turns (H18/H19 / Pattern A: 質問は末尾固定).
6768
6894
  let askHtml = '';
6769
- 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)) {
6770
6908
  const chips = (g.question.options || []).map((o) => `<button class="achip" data-fsanswer="${g.id}" data-a="${esc(o)}">${esc(o)}</button>`).join('');
6771
- // QUESTION card (H18): mono label + prompt + option chips + always-on free-text
6772
- // answer + hover trash. Data hooks (data-fsanswer / -ansinput / -anssend /
6773
- // -answerdel) are unchanged so the existing wiring below keeps working.
6774
- 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>`
6775
6920
  + `<div class="cc-ask-q">${esc(g.question.text)}</div>`
6776
6921
  + (chips ? `<div class="qchips">${chips}</div>` : '')
6777
6922
  + `<div class="answ-inline"><input type="text" data-fsansinput="${g.id}" placeholder="Type your answer…" aria-label="Type your answer">`
6778
- + `<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>`
6779
- + `<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>`;
6780
6927
  }
6781
6928
  // Clawd states (§4.5, driven by REAL state — no timers): activity in this column
6782
6929
  // → speaker row (Clawd + Claude Code + goal metadata) above the raw CC lines;
@@ -6861,7 +7008,7 @@ function renderFsFeed(){
6861
7008
  };
6862
7009
  // Delete a "needs your answer" card outright (restores 「Needs your answer」カードを削除
6863
7010
  // できるように — a question you don't want to answer had no discard path in flagship).
6864
- 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).
6865
7012
  }
6866
7013
  async function fsAnswerQuestion(goalId, answer){
6867
7014
  if (!answer) return;
@@ -9409,6 +9556,8 @@ function closeSummaryPera(){ $('summaryOverlay').classList.remove('show'); }
9409
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 };
9410
9557
  // Same convention as the existing ?dev=1 devbar flag — off unless someone opts in by URL.
9411
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';
9412
9561
  function saReplyAttachmentsHtml(goalId){
9413
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('');
9414
9563
  }
@@ -9473,6 +9622,33 @@ function latestGoalRunForOutcome(goalId, tasks, outcome){
9473
9622
  note: prev.run?.note ? `${note} ${prev.run.note}` : note,
9474
9623
  };
9475
9624
  }
9625
+ function canonicalRequirementCoverage(goal, tasks){
9626
+ const requirements = Array.isArray(goal?.requirements) ? goal.requirements.filter((r) => r?.id && r?.text) : [];
9627
+ if (!requirements.length) {
9628
+ return (tasks || []).filter((t) => t.goalId === goal?.id && (t.detail || t.title) && (!t.reply || !t.replyFrom || t.replyFrom === 'you'))
9629
+ .sort((a, b) => (a.num ?? 0) - (b.num ?? 0))
9630
+ .map((t) => ({ id: `legacy-task-${t.id}`, text: String((t.reply ? t.detail : t.title) ?? t.title ?? '').replace(/\s+/g, ' ').trim(), coverage: 'unverified', legacy: true }));
9631
+ }
9632
+ const attemptOrder = new Map((goal.attempts || []).map((attempt, index) => [attempt.id, index]));
9633
+ const evidence = Array.isArray(goal.requirementEvidence) ? goal.requirementEvidence : [];
9634
+ return requirements.map((requirement) => {
9635
+ const rows = evidence.filter((row) => row?.requirementId === requirement.id)
9636
+ .sort((a, b) => (attemptOrder.get(a.attemptId) ?? -1) - (attemptOrder.get(b.attemptId) ?? -1));
9637
+ const latestAttemptId = rows.at(-1)?.attemptId;
9638
+ const latestRows = latestAttemptId ? rows.filter((row) => row.attemptId === latestAttemptId) : [];
9639
+ const statuses = latestRows.map((row) => row.coverage);
9640
+ const coverage = statuses.includes('failed') ? 'failed'
9641
+ : statuses.includes('verified') ? 'verified'
9642
+ : statuses.length && statuses.every((status) => status === 'not-applicable') ? 'not-applicable'
9643
+ : 'unverified';
9644
+ const claims = [...new Set(latestRows.map((row) => row.claim).filter(Boolean))];
9645
+ const workerEvidence = [...new Set(latestRows.map((row) => row.workerEvidence || row.evidence).filter(Boolean))];
9646
+ const checks = latestRows.flatMap((row) => Array.isArray(row.checks) ? row.checks : [])
9647
+ .filter((check) => Array.isArray(check.requirementIds) && check.requirementIds.includes(requirement.id));
9648
+ const checkFacts = [...new Set(checks.map((check) => `${check.kind}: ${check.status}`))];
9649
+ return { id: requirement.id, text: requirement.text, coverage, claims, workerEvidence, checkFacts, attemptId: latestAttemptId ?? null };
9650
+ });
9651
+ }
9476
9652
  // One digest-row → one See-all card item. Extracted from saRows (2026-07-24) so a single
9477
9653
  // "Needs you" goal can be opened as the SAME review card (saLedgerRows solo mode) — the card's
9478
9654
  // whole shape (Request/Result/Proof/Activity, empty sections auto-hidden) is reused verbatim.
@@ -9522,10 +9698,7 @@ function saRowFromDigest(r, gs, ts){
9522
9698
  // simply omit it, so the UI remains backward compatible during rollout.
9523
9699
  attempt: g?.reviewAttempt ?? null,
9524
9700
  unmappedChangedFiles: outcome?.unmappedChangedFiles ?? [],
9525
- requirementVerification: outcome?.requirementVerification ?? null,
9526
- requirementCoverage: ts.filter((t) => t.goalId === g?.id && (t.detail || t.title)).sort((a, b) => (a.num ?? 0) - (b.num ?? 0)).map((t) => ({ text: String((t.reply ? t.detail : t.title) ?? t.title ?? '').replace(/\s+/g, ' ').trim(), taskStatus: t.status, evidenceStatus: t.requirementEvidence?.requirements?.[0]?.status ?? null })),
9527
- requirementEvidence: outcome?.requirementEvidence?.requirements ?? [],
9528
- requirementCoverage: ts.filter((t) => t.goalId === g?.id && (t.detail || t.title) && (!t.reply || !t.replyFrom || t.replyFrom === 'you')).sort((a, b) => (a.num ?? 0) - (b.num ?? 0)).map((t) => ({ text: String((t.reply ? t.detail : t.title) ?? t.title ?? '').replace(/\s+/g, ' ').trim(), status: t.status })),
9701
+ requirementCoverage: canonicalRequirementCoverage(g, ts),
9529
9702
  did, siblings, risk: g?.risk ?? null,
9530
9703
  verificationInfraError: g?.verificationInfraError ?? r.verificationInfraError ?? null,
9531
9704
  prNote: g?.prNote ?? null,
@@ -9946,7 +10119,7 @@ function saReqStackRows(it, attemptEvidence){
9946
10119
  const reqMsgs = SA.msgs[gk];
9947
10120
  const reqAsks = Array.isArray(reqMsgs) ? reqMsgs.filter((m) => m?.from === 'you' && String(m.text || '').trim()) : [];
9948
10121
  const reqItems = reqAsks.length ? reqAsks : (it.goalText ? [{ text: it.goalText }] : []);
9949
- return reqItems.map((m, idx) => {
10122
+ const row = (m, idx) => {
9950
10123
  const text = String(m.text || '').trim();
9951
10124
  const isLatest = idx === reqItems.length - 1;
9952
10125
  const evLine = isLatest && attemptEvidence ? `<p class="rqev">${esc(attemptEvidence)}</p>` : '';
@@ -9960,7 +10133,19 @@ function saReqStackRows(it, attemptEvidence){
9960
10133
  })()
9961
10134
  : `<p class="rbody rask">${esc(text)}</p>`;
9962
10135
  return `<div class="rqit${isLatest ? ' latest' : ''}"><span class="rqn">${idx + 1}</span><div class="rqbody">${body}${evLine}</div></div>`;
9963
- }).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('');
9964
10149
  }
9965
10150
  function saItemHtml(it, i){
9966
10151
  // st===3 is NOT a terminal row (REVIEW-FEEDBACK-FLOW 決定3): a reply is not a dismissal.
@@ -10081,31 +10266,27 @@ function saItemHtml(it, i){
10081
10266
  const reqSec = reqStackRows
10082
10267
  ? `<section class="rsec rreq" data-reqmsgs="${gk}"><div class="rsec-h">Request</div><div class="rqlist">${reqStackRows}</div></section>`
10083
10268
  : '';
10084
- const reqVerify = it.requirementVerification;
10085
- const reqVerifyParts = [
10086
- reqVerify?.proof?.verified ? 'Independent proof recorded' : reqVerify?.proof ? 'Proof recorded' : '',
10087
- reqVerify?.run?.url ? `Open it: ${reqVerify.run.url}` : '',
10088
- Array.isArray(reqVerify?.changedFiles) && reqVerify.changedFiles.length ? `Files: ${reqVerify.changedFiles.join(' · ')}` : '',
10089
- ].filter(Boolean);
10090
- const reqVerifySec = reqVerifyParts.length ? `<section class="rsec"><div class="rsec-h">Requirement verification</div><p class="rbody v">${esc(reqVerifyParts.join(' · '))}</p></section>` : '';
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
+ : '';
10091
10278
  const coverage = (it.requirementCoverage || []).filter((r) => r.text);
10092
- // For one requirement copied from Request, its status is useful but the full
10093
- // sentence is not. Multiple or narrowed requirements retain their text.
10094
- const coverageIsSingleRequest = coverage.length === 1
10095
- && String(coverage[0].text).replace(/\s+/g, ' ').trim() === requestText;
10096
10279
  const coverageRows = coverage.map((r) => {
10097
- const status = r.evidenceStatus || r.taskStatus;
10098
- const tone = status === 'addressed' || status === 'done' ? 'ok' : status === 'partial' ? '' : 'dim';
10099
- return `<p class="rbody v"><b class="${tone}">${esc(status)}</b>${coverageIsSingleRequest ? '' : ` · ${esc(r.text)}`}</p>`;
10280
+ const labels = { verified: 'Verified', failed: 'Failed', 'not-applicable': 'Not applicable', unverified: 'Not verified' };
10281
+ const details = [
10282
+ r.legacy ? 'Legacy item no canonical evidence was recorded' : '',
10283
+ r.checkFacts?.length ? `Manager: ${r.checkFacts.join(' · ')}` : '',
10284
+ r.claims?.length ? `Worker claim: ${r.claims.join(', ')}` : '',
10285
+ r.workerEvidence?.length ? r.workerEvidence.join(' · ') : '',
10286
+ ].filter(Boolean);
10287
+ return `<div class="rqcov-row" data-requirement-id="${esc(r.id)}"><div class="rqcov-main"><div class="rqcov-text">${esc(r.text)}</div><span class="rqcov-state ${esc(r.coverage)}">${esc(labels[r.coverage] || labels.unverified)}</span></div>${details.length ? `<div class="rqcov-detail">${details.map((detail, index) => index === 0 ? esc(detail) : `<br>${esc(detail)}`).join('')}</div>` : ''}</div>`;
10100
10288
  }).join('');
10101
- const coverageSec = coverageRows ? `<section class="rsec"><div class="rsec-h">Requirement coverage</div>${coverageRows}</section>` : '';
10102
- // Worker-declared evidence is displayed verbatim as an auditable label, not
10103
- // promoted into a Manager verdict. Empty means the worker did not provide it.
10104
- const evidenceRows = (it.requirementEvidence || []).map((entry) => {
10105
- const tone = entry.status === 'addressed' ? 'ok' : entry.status === 'partial' ? '' : 'dim';
10106
- return `<p class="rbody v"><b class="${tone}">${esc(entry.status)}</b> · ${esc(entry.evidence || entry.id)}</p>`;
10107
- }).join('');
10108
- const evidenceSec = evidenceRows ? `<section class="rsec"><div class="rsec-h">Requirement evidence</div>${evidenceRows}</section>` : '';
10289
+ const coverageSec = coverageRows ? `<section class="rsec"><div class="rsec-h">Requirement coverage</div><div class="rqcov">${coverageRows}</div></section>` : '';
10109
10290
  // ── Open it (Masa 2026-07-22): a plain section in the card's own typography — the same
10110
10291
  // heading + body shape as Request and Result, not a chip in another vocabulary. The
10111
10292
  // address the worker declared IS the control (press it → the process starts on demand →
@@ -10214,9 +10395,7 @@ function saItemHtml(it, i){
10214
10395
  const boxed = shotPair ? SA.shotBox[it.goalId] : null;
10215
10396
  const shotsBody = shotPair
10216
10397
  ? `<div class="rshotpair" data-shotpair="${it.goalId}">${typeof boxed === 'string' ? boxed : saPlainPairHtml(shotPair)}</div>`
10217
- : it.shots?.length
10218
- ? 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('')
10219
- : '';
10398
+ : saShotsListHtml(it.shots);
10220
10399
  // ── Proof (spec §6/§11): before/after (identical size) when a real image exists; a
10221
10400
  // run-log for logic-only tests; dropped entirely for a conversational task. ──
10222
10401
  let proofInner = '';
@@ -10351,12 +10530,11 @@ function saItemHtml(it, i){
10351
10530
  <div class="rhead"><span class="rtag">${it.no != null ? `#${it.no}` : ''}</span>${rst}</div>
10352
10531
  <h2 class="rtitle">${esc(it.title)}</h2>
10353
10532
  ${meta}
10533
+ ${summarySec}
10354
10534
  ${reqSec}
10355
10535
  ${S('Open it', openBody + infraBody)}
10356
10536
  ${S('What to check', checkBody)}
10357
10537
  ${coverageSec}
10358
- ${evidenceSec}
10359
- ${reqVerifySec}
10360
10538
  ${S('Run state', interruptedBody)}
10361
10539
  ${S('PR', prRepairBody)}
10362
10540
  ${S('Also carries', carriesBody)}
@@ -10460,6 +10638,25 @@ async function saEnhanceShotBoxes(){
10460
10638
  if (host) host.innerHTML = html;
10461
10639
  }));
10462
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
+ }
10463
10660
  function saRender(){
10464
10661
  const S = $('seeall'); if (!S) return;
10465
10662
  // SSE, diff fetches and action responses rebuild the dialog. Remember the
@@ -10514,7 +10711,8 @@ function saRender(){
10514
10711
  // a synthetic "new review just arrived" render the same way saSync does mid-read.
10515
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>' : '';
10516
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>
10517
- <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>`;
10518
10716
  // Most SSE goal/task events are unrelated to the card the person is reading.
10519
10717
  // Replacing #seeall with identical markup destroyed and recreated every node,
10520
10718
  // producing a blank flash and resetting browser-owned state. Keep the current
@@ -10538,6 +10736,7 @@ function saRender(){
10538
10736
  SA.lastMarkup = markup;
10539
10737
  S.innerHTML = markup;
10540
10738
  const body = S.querySelector('.sa-body');
10739
+ renderSaRail(pending, body);
10541
10740
  let focusPlacedNow = false;
10542
10741
  // H14's "scroll the focused card to top" used to always run one requestAnimationFrame
10543
10742
  // after this point. That's still required for the very FIRST render of a fresh
@@ -10709,7 +10908,7 @@ function saOpen(focusGoalId, solo = false){
10709
10908
  saFetchDiffs();
10710
10909
  saEnhanceShotBoxes();
10711
10910
  }
10712
- 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(); }
10713
10912
  // Continuous review (Masa 2026-07-08): move a persistent "current" card with
10714
10913
  // ↑↓ and auto-advance to the next pending after each verdict — same screen, same
10715
10914
  // Ledger, so approving flows without hunting for the next one.
package/engine/server.mjs CHANGED
@@ -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';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@galda/cli",
3
- "version": "0.10.104",
3
+ "version": "0.10.106",
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": {