@galda/cli 0.10.70 → 0.10.71

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
@@ -1109,6 +1109,7 @@
1109
1109
  /* 3列カンバン: chat and the task board share the width evenly beside projects */
1110
1110
  body[data-layout="kanban"] .chat{flex:1 1 0}
1111
1111
  body[data-layout="kanban"] .side{flex:1 1 0;width:auto}
1112
+ body[data-layout="kanban"] .side.collapsed{flex:0 0 0;width:0!important}
1112
1113
  /* B · Center-flow — the final layout. Center = To Do (#tasklist) + composer
1113
1114
  under it; right = Review (top) + Implementing (bottom). Populated by moving
1114
1115
  existing nodes (positionLayout) so all event wiring survives. */
@@ -1782,6 +1783,9 @@
1782
1783
  /* list rail add-row — padding-left:8px puts + on the .trow ● (8px pad + 13px ● + 9px gap grid) */
1783
1784
  #fsRoot .qadd{all:unset;box-sizing:border-box;display:flex;align-items:center;gap:9px;width:100%;padding:6px 4px 6px 8px;border-radius:8px;color:var(--ink3);font-size:11.5px;cursor:pointer;opacity:.45;transition:opacity .15s,color .15s}
1784
1785
  #fsRoot .qadd:hover{opacity:1;color:var(--ink)}
1786
+ #fsRoot .qadd.editing{opacity:1;color:var(--ink);cursor:text}
1787
+ #fsRoot .qadd input{all:unset;display:block;min-width:0;flex:1;color:var(--ink);font:inherit;line-height:18px}
1788
+ #fsRoot .qadd input::placeholder{color:var(--ink3)}
1785
1789
  #fsRoot .lateridle{display:flex;align-items:center;gap:9px;margin:5px 0 2px;padding:9px 10px;border:1px solid var(--hair);border-radius:8px;color:var(--ink2);font-size:10.5px;line-height:1.45}
1786
1790
  #fsRoot .lateridle .msg{flex:1;min-width:0}
1787
1791
  #fsRoot .lateridle button{height:26px;padding:0 10px;border:1px solid var(--hair2);border-radius:7px;background:var(--panel);color:var(--ink);font:600 10px/1 var(--mono);cursor:pointer;white-space:nowrap}
@@ -1824,11 +1828,6 @@
1824
1828
  #fsRoot .revhd[hidden]{display:none}
1825
1829
  #fsRoot .revhd .hd{font-size:13.5px;letter-spacing:-.02em}
1826
1830
  #fsRoot .revhd .rnum{font-size:10px;color:var(--ink3)}
1827
- #fsRoot .rvbtns{margin-left:auto;display:flex;gap:2px}
1828
- #fsRoot .rvb{width:22px;height:20px;border:0;border-radius:6px;background:transparent;color:var(--ink3);cursor:pointer;display:grid;place-items:center;opacity:.75}
1829
- #fsRoot .rvb svg{width:13px;height:13px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}
1830
- #fsRoot .rvb:hover{background:transparent;color:var(--ink)} /* icon hover = glyph colour only (§5.5) */
1831
- #fsRoot .rvb.on{background:var(--hover);color:var(--ink);opacity:1}
1832
1831
  /* V3-C (Masa決定 2026-07-07): the collapsed "N ready to review" strip is REMOVED —
1833
1832
  it duplicated the sm2 meter directly above. Reviews render as meter + header + cards;
1834
1833
  zero reviews render nothing at all (§1.5-3). */
@@ -1859,37 +1858,6 @@
1859
1858
  #fsRoot .rcard .b{width:21px;height:21px;border-radius:7px;border:1px solid var(--hair2);background:transparent;display:grid;place-items:center;cursor:pointer;color:var(--green);font-size:11px;font:inherit}
1860
1859
  #fsRoot .rcard .b.no{color:var(--danger)}
1861
1860
  #fsRoot .rcard .b:hover{background:var(--hover)}
1862
- /* rv2 compact one-line rows */
1863
- #fsRoot[data-rv="2"] .rcard{margin-top:0;border:0;border-radius:0;border-bottom:1px solid var(--hair);padding:7px 3px;gap:9px}
1864
- #fsRoot[data-rv="2"] .rcard img{display:none}
1865
- #fsRoot[data-rv="2"] .rcard .rc{display:flex;align-items:center;gap:8px}
1866
- #fsRoot[data-rv="2"] .rcard .t{font-weight:400;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
1867
- #fsRoot[data-rv="2"] .rcard .pr{margin-top:0;flex:0 0 auto}
1868
- #fsRoot[data-rv="2"] .rcard .rid{margin:0;flex:0 0 auto;order:-1}
1869
- #fsRoot[data-rv="2"] .rcard .b{width:22px;height:22px}
1870
- /* rv3 large proof thumb as hero (default) — legibility scrim is a faithful flagship port.
1871
- CDO review 2026-07-08 §3: was scoped :not(.noimg) only, so a proof-less review (no PNG/GIF
1872
- yet) fell through to the plain white/dark-text .rcard base and floated among the dark
1873
- thumbnail cards. Now EVERY rv3 card (img or not) gets the same gallery container language —
1874
- dark ground + white heading + scrim — .noimg just swaps the <img> for a placeholder-dark
1875
- background (no image element to size the card, so it also needs its own min-height). */
1876
- /* min-height + dark ground live on the BASE card (not just .noimg): the card's
1877
- height must NOT depend on the proof <img> rendering. When a proof PNG/GIF is
1878
- slow (loading="lazy") or fails (404/expired key), the img collapsed below 84px,
1879
- so the bottom-anchored .rc scrim (26px top padding) overflowed UPWARD and got
1880
- clipped by overflow:hidden → the "レビューがずれる" clipped dark bars. Now every
1881
- rv3 card reserves 84px over a dark ground; the img just fills it when present,
1882
- and a broken img (onerror → removed in JS) reveals the same dark ground. */
1883
- #fsRoot[data-rv="3"] .rcard{display:block;position:relative;margin-top:8px;padding:0;border:0;border-radius:12px;overflow:hidden;box-shadow:0 0 0 1px var(--hair);min-height:84px;background:linear-gradient(155deg,#2b2e37,#15161a)}
1884
- #fsRoot[data-rv="3"] .rcard img{width:100%;height:84px;border-radius:0;box-shadow:none;object-fit:cover;display:block}
1885
- #fsRoot[data-rv="3"] .rcard.noimg{min-height:84px;background:linear-gradient(155deg,#2b2e37,#15161a)} /* placeholder-dark ground, no CSS-blob/lorem — a flat tonal fill matching the scrim's own black, not a fake image */
1886
- #fsRoot[data-rv="3"] .rcard .rc{position:absolute;left:0;right:0;bottom:0;padding:26px 13px 11px;background:linear-gradient(0deg,rgba(8,10,14,.93),rgba(8,10,14,.72) 45%,rgba(8,10,14,.28) 80%,transparent)} /* §15 stronger scrim for bright thumbs/2-line */
1887
- #fsRoot[data-rv="3"] .rcard .t{color:#fff;font-size:11.5px;line-height:1.25;text-shadow:0 1px 3px rgba(0,0,0,.55)} /* §15 white heading readable on any photo (or the noimg placeholder ground) */
1888
- #fsRoot[data-rv="3"] .rcard .pr{color:#e9fff6;background:rgba(255,255,255,.16)}
1889
- #fsRoot[data-rv="3"] .rcard .rid{color:rgba(255,255,255,.66)}
1890
- #fsRoot[data-rv="3"] .rcard .act{position:absolute;top:9px;right:9px}
1891
- #fsRoot[data-rv="3"] .rcard .b{background:rgba(255,255,255,.92);border-color:transparent;color:var(--green)}
1892
- #fsRoot[data-rv="3"] .rcard .b.no{color:#c0392b}
1893
1861
  /* fold line — "+N more ›" 統一 (§6.5) */
1894
1862
  #fsRoot .morefold{display:flex;justify-content:center;text-align:center;font-family:var(--mono);font-size:10.5px;letter-spacing:.02em;color:var(--ink3);cursor:pointer;padding:8px;border-radius:8px;align-items:center}
1895
1863
  #fsRoot .morefold:hover{background:var(--hover-s);color:var(--ink)}
@@ -3106,7 +3074,7 @@
3106
3074
  Structure ported from docs/design/flagship/flagship.html: rail | Implementing | lane.
3107
3075
  The composer (#composerWrap: queue + composer + send/prism ring) is ADOPTED into
3108
3076
  #fsCompwrap by positionLayout() — same nodes, same handlers, input-focus fix intact. -->
3109
- <div id="fsRoot" data-rv="1">
3077
+ <div id="fsRoot">
3110
3078
  <div class="fscard fsrail">
3111
3079
  <button class="railtg" id="fsRailTg" title="Fold the rail"></button><!-- panel icon filled by tgUpdate (v45 §4) -->
3112
3080
  <button class="fsprojbtn" id="fsProjBtn" title="Switch project"><span class="pname" id="fsProjBtnName"></span><svg viewBox="0 0 24 24"><path d="M6 9l6 6 6-6"/></svg></button>
@@ -6324,7 +6292,6 @@ function render(){
6324
6292
  // queueOrder); actions call the SAME endpoints (approve/dismiss/retry/reply…).
6325
6293
  // ============================================================================
6326
6294
  const fsUI = {
6327
- rv: 1, // approved screenshot-free B Status dot design; Review has one stable view
6328
6295
  band: Number(localStorage.getItem('fsBand') ?? 0), // theme-6 band choice
6329
6296
  revMore: false, // "+N more" fold on the review list (§1.5-8)
6330
6297
  todoFold: false, doneOpen: false, rejOpen: false, todoMore: false,
@@ -6352,7 +6319,6 @@ function fsActs(goalId){
6352
6319
  function renderFlagship(){
6353
6320
  if (state.layout !== 'flagship') return;
6354
6321
  const root = $('fsRoot'); if (!root) return;
6355
- root.dataset.rv = String(fsUI.rv);
6356
6322
  // Wall-clock phase for the spinning markers: any ring re-created this render (the board
6357
6323
  // re-renders often on progress %) starts at the angle a continuous one would be at now,
6358
6324
  // instead of snapping back to 0deg — no "jump". The rail ring is guarded separately and
@@ -6933,8 +6899,10 @@ function renderFsReview(){
6933
6899
  const pgoals = state.goals.filter((g) => g.projectId === state.active);
6934
6900
  const list = state.tasks.filter((t) => t.projectId === state.active);
6935
6901
  const rows = buildReviewDigest({ goals: pgoals, tasks: list });
6936
- const foldedRows = pgoals.filter((g) => g.status === 'folded' && g.foldedInto != null);
6937
- if (!rows.length && !foldedRows.length) { el.innerHTML = ''; return; } // §1.5-3: no reviews the UI itself does not exist
6902
+ // Folded goals belong only to the carrier's Review dialog ("Also carries").
6903
+ // They are historical context, not independently reviewable work, so rendering
6904
+ // them here produced the contradictory "0 ready" header followed by stale cards.
6905
+ if (!rows.length) { el.innerHTML = ''; return; } // §1.5-3: no reviews → the UI itself does not exist
6938
6906
  const green = rows.filter((r) => !r.testResult || r.testResult.ok !== false).length;
6939
6907
  const blockedN = pgoals.filter((g) => g.status === 'blocked').length;
6940
6908
  const pct = rows.length ? Math.round((green / rows.length) * 100) : 0;
@@ -6974,13 +6942,6 @@ function renderFsReview(){
6974
6942
  return `<div class="rcard noimg" data-fsrev="${r.goalIds[0]}"><div class="rc"><div class="t">${rid}${esc(r.plan ? r.checkLine : saReviewTitle(g0))}</div>${rnote}${infraNote}${pr}${conflictNote}</div>
6975
6943
  <div class="act"><button class="b" data-fsapprove="${ids}" title="Approve">✓</button><button class="b no" data-fsdismiss="${ids}" title="Send back">✕</button></div></div>`;
6976
6944
  };
6977
- const foldCard = (g) => {
6978
- const target = pgoals.find((x) => x.id === g.foldedInto);
6979
- const n = goalReviewNumber({ goal: g, tasks: list });
6980
- const rid = n != null ? `<span class="rid gno" data-n="${n}">#${n}</span>` : '';
6981
- const targetNo = target ? (goalReviewNumber({ goal: target, tasks: list }) ?? target.id) : g.foldedInto;
6982
- return `<div class="rcard noimg foldrow" data-fsrev="${g.id}"><div class="rc"><div class="t">${rid}${esc(saReviewTitle(g))}</div><span class="rstat">Merged into #${esc(String(targetNo))}</span></div></div>`;
6983
- };
6984
6945
  // V3-C (Masa決定 2026-07-07): no collapsed strip — it duplicated the sm2 meter right
6985
6946
  // above. Reviews render directly: meter + Review header + cards (+N more fold, §1.5-8).
6986
6947
  // The meter counts what is reviewable, so it only exists when something is (§1.5-3):
@@ -6991,7 +6952,7 @@ function renderFsReview(){
6991
6952
  <div class="m-bar"><i style="width:${pct}%"></i></div>
6992
6953
  <div class="s">${esc(sline)}</div></div>` : ''}
6993
6954
  <div class="revhd" id="fsRevHd"><span class="hd">Review</span><span class="rnum num">${green} ready</span></div>
6994
- <div class="revlist">${shown.map(card).join('')}${foldedRows.map(foldCard).join('')}
6955
+ <div class="revlist">${shown.map(card).join('')}
6995
6956
  ${rows.length > RCAP ? `<div class="morefold" id="fsRevMore">${fsUI.revMore ? 'less' : `+${rows.length - RCAP} more`}<span class="g">›</span></div>` : ''}</div>`;
6996
6957
  // Mirror the ready-review count onto the mobile top-bar "Review N" opener (H23).
6997
6958
  const _hn = document.getElementById('fsHambN'); if (_hn) _hn.textContent = green > 0 ? String(green) : '';
@@ -7009,8 +6970,8 @@ function renderFsReview(){
7009
6970
  if (e.target.closest('a') || e.target.closest('.act') || e.target.closest('[data-goalreverify]')) return;
7010
6971
  openReviewChecklist(Number(c.dataset.fsrev));
7011
6972
  };
7012
- // Folded goals get no row here (Masa 2026-07-23) — they are shown, and restored, on the
7013
- // carrier's side in the See-all "Also carries" section (saItemHtml + the seeall handler).
6973
+ // Folded goals are shown, and restored, on the carrier's side in the See-all
6974
+ // "Also carries" section (saItemHtml + the seeall handler).
7014
6975
  for (const b of el.querySelectorAll('[data-fsapprove]')) b.onclick = async (e) => { e.stopPropagation();
7015
6976
  let ok = true, mergeGoal = null;
7016
6977
  for (const id of b.dataset.fsapprove.split(',')) {
@@ -7289,9 +7250,9 @@ function renderFsTodo(){
7289
7250
  ${rejected.length ? `<span class="tlcap rejcap"${(doingRows || paused) || needsRows ? '' : ' style="padding-top:0"'}>Dismissed<span class="num">${rejected.length}</span></span>${rejected.map((g) => `<div class="trow up rej" data-fsgoalrow="${g.id}"><span class="st rej"></span>${noSpan(g.id)}<span class="tt">${tt(g.text, 70)}</span><span class="acts"><button class="ab" data-fsreopen="${g.id}" title="Reopen — back to Review">${FS_ICONS.reopen}</button><button class="ab" data-fsrejdel="${g.id}" title="Delete — discard (closes the PR)">${FS_ICONS.del}</button></span></div>`).join('')}` : ''}
7290
7251
  ${(parkedRows || upRows) ? `<span class="tlcap"${(doingRows || paused) || needsRows || rejected.length ? '' : ' style="padding-top:0"'}>Next up</span>${parkedRows}${upRows}` : ''}
7291
7252
  ${queued.length > NCAP ? `<div class="morefold" id="fsTodoMore">${fsUI.todoMore ? 'less' : `+${queued.length - NCAP} more`}<span class="g">›</span></div>` : ''}
7292
- <button class="qadd" data-fsnew="1" title="New goal"><span class="paplus"></span>New</button>
7253
+ <div class="qadd" data-fsnew="1" role="button" tabindex="0" title="New goal"><span class="paplus"></span><span>New</span></div>
7293
7254
  <span class="tlcap">Later</span>
7294
- <button class="qadd lateradd" data-fslaternew="1" title="New Later goal"><span class="paplus"></span>New</button>
7255
+ <div class="qadd lateradd" data-fslaternew="1" role="button" tabindex="0" title="New Later goal"><span class="paplus"></span><span>New</span></div>
7295
7256
  ${laterIdle}
7296
7257
  ${laterRows}
7297
7258
  </div>
@@ -7299,12 +7260,12 @@ function renderFsTodo(){
7299
7260
  <div id="fsDonelist"${fsUI.doneOpen ? '' : ' hidden'}>${doneAll.map((t) => `<div class="trow up dim done"${t.goalId != null ? ` data-fsgoalrow="${t.goalId}"` : ''}><span class="st ok"></span>${noSpan(t.goalId)}<span class="tt">${tt(t.title)}</span></div>`).join('')}</div>` : ''}`;
7300
7261
  el.classList.toggle('folded', !!fsUI.todoFold);
7301
7262
  $('fsTodoHd').onclick = () => { fsUI.todoFold = !fsUI.todoFold; el.classList.toggle('folded', fsUI.todoFold); };
7302
- const qadd = el.querySelector('.qadd[data-fsnew]'); // + New → focus the composer (the real create path)
7303
- if (qadd) qadd.onclick = () => $('input')?.focus();
7263
+ const qadd = el.querySelector('.qadd[data-fsnew]');
7264
+ if (qadd) qadd.onclick = (e) => { e.stopPropagation(); fsInlineNewRow(qadd, 'Add to To Do…', (text) => fsCreateGoal(text, false)); };
7304
7265
  const laterAdd = el.querySelector('.qadd[data-fslaternew]');
7305
7266
  if (laterAdd) laterAdd.onclick = (e) => {
7306
7267
  e.stopPropagation();
7307
- fsInlineInput(laterAdd, '', 'Add to Later…', fsCreatePendingGoal);
7268
+ fsInlineNewRow(laterAdd, 'Add to Later…', (text) => fsCreateGoal(text, true));
7308
7269
  };
7309
7270
  const dl = el.querySelector('#fsDoneLine');
7310
7271
  if (dl) dl.onclick = () => { fsUI.doneOpen = !fsUI.doneOpen; dl.classList.toggle('open', fsUI.doneOpen); el.querySelector('#fsDonelist').hidden = !fsUI.doneOpen; };
@@ -7381,19 +7342,50 @@ function renderFsTodo(){
7381
7342
  const r = await fetch(withKey(`/api/goals/${b.dataset.fsdel}`), { method: 'DELETE' });
7382
7343
  if (!r.ok) showErr('Cannot delete a running/finished goal — reply to append instead'); else await refresh(); };
7383
7344
  }
7384
- async function fsCreatePendingGoal(text){
7345
+ async function fsCreateGoal(text, pending){
7385
7346
  const r = await fetch(withKey('/api/tasks'), { method: 'POST', headers: { 'content-type': 'application/json' },
7386
- body: JSON.stringify({ projectId: state.active, text, pr: 'auto', model: state.stickyModel, effort: state.stickyEffort, agent: state.connectedApp, mode: 'auto', pending: true }) });
7347
+ body: JSON.stringify({ projectId: state.active, text, pr: 'auto', model: state.stickyModel, effort: state.stickyEffort, agent: state.connectedApp, mode: 'auto', pending }) });
7387
7348
  if (r.status === 402) {
7388
7349
  const data = await r.json().catch(() => ({}));
7389
7350
  showBillingBlocked(data.blocked);
7390
- return;
7351
+ return false;
7391
7352
  }
7392
- if (!r.ok) { showErr('Later add failed.'); return; }
7353
+ if (!r.ok) { showErr(pending ? 'Later add failed.' : 'To Do add failed.'); return false; }
7393
7354
  clearBillingBlocked();
7394
7355
  const goal = await r.json();
7395
7356
  if (goal?.id) upsert(state.goals, goal);
7396
7357
  render();
7358
+ return true;
7359
+ }
7360
+ function fsInlineNewRow(row, placeholder, onSubmit){
7361
+ if (!row) return;
7362
+ const current = row.querySelector('input');
7363
+ if (current) { current.focus(); return; }
7364
+ const label = row.querySelector('span:last-child');
7365
+ if (!label) return;
7366
+ const original = label.textContent;
7367
+ const inp = document.createElement('input');
7368
+ inp.type = 'text'; inp.placeholder = placeholder; inp.setAttribute('aria-label', placeholder);
7369
+ label.replaceWith(inp); row.classList.add('editing'); inp.focus();
7370
+ let done = false;
7371
+ const restore = () => {
7372
+ if (done) return;
7373
+ done = true; row.classList.remove('editing');
7374
+ if (inp.isConnected) { const span = document.createElement('span'); span.textContent = original; inp.replaceWith(span); }
7375
+ };
7376
+ inp.onkeydown = (e) => {
7377
+ if (e.key === 'Enter' && !e.isComposing) {
7378
+ e.preventDefault();
7379
+ const value = inp.value.trim();
7380
+ if (!value) return;
7381
+ done = true; row.classList.remove('editing'); inp.disabled = true;
7382
+ Promise.resolve(onSubmit(value)).then((ok) => {
7383
+ if (ok !== false || !inp.isConnected) return;
7384
+ done = false; row.classList.add('editing'); inp.disabled = false; inp.focus();
7385
+ });
7386
+ } else if (e.key === 'Escape') restore();
7387
+ };
7388
+ inp.onblur = () => setTimeout(restore, 150);
7397
7389
  }
7398
7390
  function fsInlineInput(afterEl, value, placeholder, onSubmit){
7399
7391
  if (!afterEl || afterEl.nextElementSibling?.classList.contains('fsinline')) return;
@@ -9613,7 +9605,9 @@ function saItemHtml(it, i){
9613
9605
  // compare-only or title-only declaration must not draw the "Start it" button.
9614
9606
  const openBody = (it.run?.cmd || it.run?.url)
9615
9607
  ? `<div class="ropen" id="runlocal${i}"><button type="button" class="olink" data-saact="preview:${i}" id="prevlink${i}">${esc(it.run.url || 'Start it')}<span class="ar">↗</span></button><p class="rbody v">${esc(it.run.note || it.run.cmd || 'Starts when you press it.')}</p></div>`
9616
- : '';
9608
+ : it.pr
9609
+ ? `<div class="ropen"><a class="olink" href="${esc(it.pr)}" target="_blank" rel="noopener">${esc(it.pr)}<span class="ar">↗</span></a><p class="rbody v">Review and merge this pull request to finish the item.</p></div>`
9610
+ : '';
9617
9611
  const prRepairBody = it.goal?.prError
9618
9612
  ? `<div class="prfail"><b>${esc(it.goal.prRepair?.title || 'PR blocked')}</b> <span class="dim">${esc(it.goal.prError)}</span>${it.goal.prRepair?.steps?.length ? `<ul>${it.goal.prRepair.steps.map((s) => `<li>${esc(s)}</li>`).join('')}</ul>` : ''}${it.goal.prRepair?.dirtyFiles?.length ? `<div class="dim">dirty: ${it.goal.prRepair.dirtyFiles.map(esc).join(' · ')}</div>` : ''}<div class="acts2"><button class="errbtn" data-saact="retrypr:${i}">${failureLang() === 'ja' ? 'PR再試行' : 'Retry PR'}</button><button class="errbtn" data-saact="glog:${i}">${failureLang() === 'ja' ? 'ログ確認' : 'View log'}</button></div></div>`
9619
9613
  : '';
package/engine/lib.mjs CHANGED
@@ -89,6 +89,36 @@ export function resolveEntryUrl({ entry, projectDir, managerRoot, port, accessKe
89
89
  return pathToFileURL(abs).href;
90
90
  }
91
91
 
92
+ // Preserve the worker-declared artifact below an ephemeral Manager origin.
93
+ export function rebasePreviewUrl(declaredUrl, serverUrl) {
94
+ if (!serverUrl) return declaredUrl ?? null;
95
+ if (!declaredUrl) return serverUrl;
96
+ try {
97
+ const declared = new URL(declaredUrl);
98
+ const server = new URL(serverUrl);
99
+ const params = new URLSearchParams(declared.search);
100
+ for (const [key, value] of server.searchParams) {
101
+ if (!params.has(key)) params.set(key, value);
102
+ }
103
+ server.pathname = declared.pathname || '/';
104
+ server.search = params.toString();
105
+ server.hash = declared.hash;
106
+ return server.toString();
107
+ } catch {
108
+ return serverUrl;
109
+ }
110
+ }
111
+
112
+ export function shouldCreateGoalPR(goal, files = []) {
113
+ return goal?.wantsPR === true && files.length > 0;
114
+ }
115
+
116
+ export function normalizePullRequestUrl(value) {
117
+ const text = typeof value === 'string' ? value.trim() : '';
118
+ if (!/^https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+\/?$/i.test(text)) return null;
119
+ return text.replace(/\/$/, '');
120
+ }
121
+
92
122
  // Extract token/cost usage from a parsed `type:'result'` stream-json event
93
123
  // (the final event of a `claude -p --output-format stream-json` run).
94
124
  // Returns null for any other event type.
@@ -482,7 +512,7 @@ export function goalSessionFor(goal, agent) {
482
512
  const a = String(agent || goal.agent || 'claude-code');
483
513
  const map = goal.sessions && typeof goal.sessions === 'object' ? goal.sessions : null;
484
514
  if (map?.[a]) return String(map[a]);
485
- if (goal.sessionId && (!goal.sessionAgent || goal.sessionAgent === a || !map)) return String(goal.sessionId);
515
+ if (goal.sessionId && !map && (!goal.sessionAgent || goal.sessionAgent === a)) return String(goal.sessionId);
486
516
  return null;
487
517
  }
488
518
 
@@ -499,7 +529,8 @@ export function setGoalAgentSession(goal, agent, sessionId) {
499
529
 
500
530
  export function switchGoalAgentSession(goal, nextAgent) {
501
531
  if (!goal) return null;
502
- if (goal.sessionId && goal.agent) setGoalAgentSession(goal, goal.agent, goal.sessionId);
532
+ const previousAgent = String(goal.agent || 'claude-code');
533
+ if (goal.sessionId) setGoalAgentSession(goal, previousAgent, goal.sessionId);
503
534
  goal.agent = nextAgent;
504
535
  const sessionId = goalSessionFor(goal, nextAgent);
505
536
  goal.sessionId = sessionId;
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 { needsProjectFolder, folderLabel, chooseFolderScript, parseChosenFolder, buildFolderQuestion, resolveFolderAnswer, classifyFolderTarget, buildFolderInitConfirm, isFolderInitConfirmed, needsReviewPreference, buildReviewPreferenceQuestion, resolveReviewPreferenceAnswer, routeQuestionAnswer, notUnderstoodNote, parseRelocalizedQuestion, classifyAuthProbe, authBannerText, makeSigninChallenge, parseClaimResponse, isCommandOnPath } from './lib.mjs';
24
- import { parseStreamEvents, parseCodexEvents, buildCodexArgs, parsePlan, refinePlanTasks, canEditGoal, canDeleteGoal, shouldAskClarification, workerExitReason, classifyWorkerFailure, isForcedStop, taskStatusAfterVerify, workerResultText, taskCountsAsComplete, resolveWantsPR, resolveGoalSource, buildGoalSummary, 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, 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, classifyComposerIntentHeuristic, detectPauseIntent, buildIntentPrompt, parseIntentResponse, buildBoardSnapshot, validateBoardSnapshot, boardIsEmpty, decideBoardPull, resolveConnectedAgents, pickAvailableAgent, pickUtilityAgent, utilityModel, liveTakeoverDecision, shouldOpenBrowserOnBoot, goalTaskTitle, buildGoalTask, shouldUsePlannerForGoal, RUN_DECL_FILE, parseRunDeclaration, SHOT_DIR, collectShotNames, parseGoalAddress, REPLY_OUTCOMES, REPLY_INTENTS, buildReplyIntentPrompt, parseReplyIntent, buildGoalMessage, serializeGoalMessage, parseGoalMessages, computeInternalQualityMetrics, classifyInternalQualityTaskError, resolveWorkspaceMode, goalSessionFor, setGoalAgentSession, switchGoalAgentSession } from './lib.mjs';
24
+ import { parseStreamEvents, parseCodexEvents, buildCodexArgs, parsePlan, refinePlanTasks, canEditGoal, canDeleteGoal, shouldAskClarification, workerExitReason, classifyWorkerFailure, isForcedStop, taskStatusAfterVerify, workerResultText, taskCountsAsComplete, resolveWantsPR, resolveGoalSource, buildGoalSummary, 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, classifyComposerIntentHeuristic, detectPauseIntent, buildIntentPrompt, parseIntentResponse, buildBoardSnapshot, validateBoardSnapshot, boardIsEmpty, decideBoardPull, resolveConnectedAgents, pickAvailableAgent, pickUtilityAgent, utilityModel, liveTakeoverDecision, shouldOpenBrowserOnBoot, goalTaskTitle, buildGoalTask, shouldUsePlannerForGoal, RUN_DECL_FILE, parseRunDeclaration, SHOT_DIR, collectShotNames, parseGoalAddress, REPLY_OUTCOMES, REPLY_INTENTS, buildReplyIntentPrompt, parseReplyIntent, buildGoalMessage, serializeGoalMessage, parseGoalMessages, computeInternalQualityMetrics, classifyInternalQualityTaskError, resolveWorkspaceMode, goalSessionFor, setGoalAgentSession, switchGoalAgentSession } from './lib.mjs';
25
25
  import { createSerialQueue } from './lib.mjs';
26
26
  import { isPrClosed } from './lib.mjs';
27
27
  import { collectProjectRules } from './lib.mjs';
@@ -2004,24 +2004,6 @@ function managerHandoffPrompt(goal, plan) {
2004
2004
  ].filter(Boolean).join('\n\n');
2005
2005
  }
2006
2006
 
2007
- function recentProjectFailureMemory(projectId, text) {
2008
- const haystack = String(text ?? '').toLowerCase();
2009
- return goals
2010
- .filter((g) => g.projectId === projectId && Array.isArray(g.failureMemory) && g.failureMemory.length)
2011
- .flatMap((g) => g.failureMemory.map((entry) => ({ goal: g, entry })))
2012
- .filter(({ entry }) => {
2013
- if (!haystack) return true;
2014
- const blob = `${entry?.recurrenceKey ?? ''} ${entry?.rootCause ?? ''} ${entry?.whatHappened ?? ''}`.toLowerCase();
2015
- return blob.includes('test') && /test|テスト|検証/.test(haystack)
2016
- || blob.includes('budget') && /token|budget|予算|コンテキスト/.test(haystack)
2017
- || blob.includes('proof') && /ui|proof|スクショ|画面/.test(haystack)
2018
- || blob.includes('non-code') && /docs?|prd|調査|レポート|document/.test(haystack);
2019
- })
2020
- .sort((a, b) => String(b.entry.createdAt ?? '').localeCompare(String(a.entry.createdAt ?? '')))
2021
- .slice(0, 3)
2022
- .map(({ entry }) => entry);
2023
- }
2024
-
2025
2007
  function recordGoalFailure(goal, { kind, reason = '', usage = null, changedFiles = [], testResult = null, attentionHadDetail = true } = {}) {
2026
2008
  if (!goal) return null;
2027
2009
  const siblings = tasks.filter((t) => t.goalId === goal.id);
@@ -2553,10 +2535,11 @@ async function startPreview(task) {
2553
2535
  if (managerPackage && existsSync(join(dir, 'engine', 'server.mjs')) && existsSync(join(dir, 'app', 'index.html'))) {
2554
2536
  const eph = await startEphemeralApp(dir);
2555
2537
  if (!eph) throw new Error('Could not start the local preview. Please try again.');
2556
- const entry = { proc: eph.child, url: eph.url, expires: Date.now() + PREVIEW_IDLE_MS };
2538
+ const previewUrl = rebasePreviewUrl(url, eph.url);
2539
+ const entry = { proc: eph.child, url: previewUrl, expires: Date.now() + PREVIEW_IDLE_MS };
2557
2540
  previews.set(task.id, entry);
2558
2541
  eph.child.on('close', () => { if (previews.get(task.id) === entry) previews.delete(task.id); });
2559
- return { url: eph.url, reused: false, started: true };
2542
+ return { url: previewUrl, reused: false, started: true };
2560
2543
  }
2561
2544
  // Declared a URL but no command: the thing is already reachable (an app the
2562
2545
  // worker left running, a hosted page). Nothing to start.
@@ -3279,11 +3262,10 @@ async function finishGoalIfComplete(goal, project, activityTask = null) {
3279
3262
  // (e.g. the skip endpoint) still PR from the isolated worktree, not the shared dir.
3280
3263
  const wdir = (goalWorkDirs.get(goal.id) && existsSync(goalWorkDirs.get(goal.id))) ? goalWorkDirs.get(goal.id) : project.dir;
3281
3264
  const prProject = { ...project, dir: wdir };
3282
- // Masa's model: any goal that TOUCHED CODE opens a PR (reviewed via the PR);
3283
- // a no-code goal is a report/confirmation. So always attempt a PR createGoalPR
3284
- // no-ops when there are no changed files instead of gating on goal.wantsPR
3285
- // (which defaulted to false, so almost nothing ever became a PR). Always call
3286
- // it (not just `if (!goal.pr)`): when a goal already has a PR — e.g. a review
3265
+ // Close-out always delegates to createGoalPR so it can record an explicit
3266
+ // No PR decision as well as create/update a requested PR. `goal.wantsPR` is
3267
+ // authoritative: changed files alone must never override the user's choice.
3268
+ // Always call it (not just `if (!goal.pr)`): when a goal already has a PR — e.g. a review
3287
3269
  // comment sent it back for rework and the reply task just finished — this is
3288
3270
  // the only place that pushes the rework diff, and createGoalPR itself decides
3289
3271
  // "new PR" vs "push onto the existing PR's branch" from goal.pr/goal.prBranch.
@@ -3626,13 +3608,16 @@ async function createGoalPR(goal, project, goalTasks, reviewDefinition = DEFAULT
3626
3608
  // the diff. Proof artifacts (.manager-proof) are intentional and kept.
3627
3609
  const SCRATCH = /(^|\/)\.tmp|\.log$|(^|\/)\.manager-wt(\/|$)|(^|\/)node_modules(\/|$)|(^|\/)secret\.key$/;
3628
3610
  const files = [...new Set(goalTasks.flatMap((t) => t.changedFiles))].filter((f) => f && !SCRATCH.test(f));
3629
- // No code changed → nothing to open a PR for. This is a confirmation /
3630
- // investigation goal, not a failure: it still lands in Review (nextGoalStatus),
3631
- // where the worker's report IS the "please confirm" summary. So clear any PR
3632
- // error and return — don't scare the reviewer with a red prError. (A rework
3633
- // round can't actually hit this — its files are a superset of the ones that
3634
- // already got it a PR — but the guard is harmless either way.)
3635
- if (!files.length) { goal.pr = null; goal.prError = null; saveGoal(goal); goalAct('No code changes detected, so PR creation is skipped.'); return; }
3611
+ if (!shouldCreateGoalPR(goal, files)) {
3612
+ goal.pr = null;
3613
+ goal.prBranch = null;
3614
+ goal.prError = null;
3615
+ saveGoal(goal);
3616
+ goalAct(goal.wantsPR === false
3617
+ ? 'Deliverable is No PR, so PR creation is skipped even though files changed.'
3618
+ : 'No code changes detected, so PR creation is skipped.');
3619
+ return;
3620
+ }
3636
3621
  const unsafe = prSafetyIssue(goal, project, files);
3637
3622
  if (unsafe) {
3638
3623
  goal.prError = unsafe.message.slice(0, 300);
@@ -4155,7 +4140,7 @@ const server = createServer(async (req, res) => {
4155
4140
  req.on('data', (d) => { body += d; });
4156
4141
  req.on('end', async () => {
4157
4142
  try {
4158
- const { projectId, text, pr, images, model, effort, source, pending, review, note, mode, skill, agent, worktree, workspaceMode } = JSON.parse(body);
4143
+ const { projectId, text, pr, existingPr, images, model, effort, source, pending, review, note, mode, skill, agent, worktree, workspaceMode } = JSON.parse(body);
4159
4144
  const project = projects.find((p) => p.id === projectId);
4160
4145
  if (!project || !text?.trim()) return json(res, 400, { error: 'projectId and text required' });
4161
4146
  // Chat-driven play/pause (Masa: "チャットで再生一時停止はおせる"). Deterministic
@@ -4269,16 +4254,20 @@ const server = createServer(async (req, res) => {
4269
4254
  // (connected-agents Phase A) — a requested/default agent that isn't
4270
4255
  // present would otherwise spawn a worker doomed to fail at launch.
4271
4256
  const goalAgent = pickAvailableAgent(workerAgent(agent), AVAILABLE_AGENTS);
4272
- const priorFailureMemory = recentProjectFailureMemory(projectId, text.trim());
4257
+ // Failure memory belongs to one conversation. A new Goal must not
4258
+ // inherit an unrelated Goal's old failure just because both mention UI.
4259
+ const priorFailureMemory = [];
4273
4260
  const resolvedWorkspaceMode = resolveWorkspaceMode(workspaceMode ?? worktree);
4274
4261
  const workspaceDirty = resolvedWorkspaceMode === 'direct-workspace' ? projectDirtyInfo(project.dir) : {};
4262
+ const importedPr = review ? normalizePullRequestUrl(existingPr) : null;
4263
+ if (review && existingPr && !importedPr) return json(res, 400, { error: 'existingPr must be a GitHub pull request URL' });
4275
4264
  const goal = {
4276
4265
  id: nextId++, projectId, text: text.trim().slice(0, 8000),
4277
4266
  status: (needsFolder || needsReviewPref) ? 'needsInput' : review ? 'review' : pending ? 'pending' : 'stacked',
4278
4267
  question: folderQuestion || reviewPrefQuestion,
4279
4268
  reviewOnly: review ? true : undefined,
4280
4269
  note: review && typeof note === 'string' && note.trim() ? note.trim().slice(0, 8000) : undefined,
4281
- wantsPR: review ? false : resolveWantsPR(pr, text, getReviewDefinition(projectId).defaultWantsPR),
4270
+ wantsPR: review ? Boolean(importedPr) : resolveWantsPR(pr, text, getReviewDefinition(projectId).defaultWantsPR),
4282
4271
  model: workerModel(goalAgent, model),
4283
4272
  effort: workerEffort(goalAgent, effort),
4284
4273
  agent: goalAgent,
@@ -4305,7 +4294,7 @@ const server = createServer(async (req, res) => {
4305
4294
  previousSession: false,
4306
4295
  }),
4307
4296
  priorFailureMemory: priorFailureMemory.length ? priorFailureMemory : undefined,
4308
- plan: null, pr: undefined, createdAt: new Date().toISOString(),
4297
+ plan: null, pr: importedPr || undefined, createdAt: new Date().toISOString(),
4309
4298
  // Stage 2 (multi-user): who this goal belongs to — 'owner' for
4310
4299
  // key-auth/MANAGER_OWNER_EMAIL, else the creator's Google email.
4311
4300
  owner: identity,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@galda/cli",
3
- "version": "0.10.70",
3
+ "version": "0.10.71",
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": {