@galda/cli 0.10.70 → 0.10.72

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)}
@@ -2152,6 +2120,12 @@
2152
2120
  /* the note is body prose like every other section's — same 14px / −.012em / --ink as
2153
2121
  .rbody (Masa 2026-07-22: it was a shade lighter and read as a different kind of text) */
2154
2122
  .redesign .ropen .v{margin:4px 0 0 !important}
2123
+ /* A malformed .manager-run.json advisory (goal #733, Masa 2026-07-26): plain
2124
+ danger-red text, no box — this must not read as calm/resolved like a normal
2125
+ note (Masa: "めっちゃdoneっぽい"). Same --danger token .prfail already uses
2126
+ for a blocked PR, so it speaks the SAME "needs your attention" language the
2127
+ app already has, rather than inventing a new one. */
2128
+ .redesign .ropen.rdeclerr .v{color:var(--danger) !important}
2155
2129
  /* sections — Linear-docs bold sentence-case headings + high-contrast body.
2156
2130
  margins are !important: the app's pre-existing .lg / .sa-item rules load first and
2157
2131
  otherwise tie/beat these on specificity, flattening the Linear-docs rhythm. */
@@ -3106,7 +3080,7 @@
3106
3080
  Structure ported from docs/design/flagship/flagship.html: rail | Implementing | lane.
3107
3081
  The composer (#composerWrap: queue + composer + send/prism ring) is ADOPTED into
3108
3082
  #fsCompwrap by positionLayout() — same nodes, same handlers, input-focus fix intact. -->
3109
- <div id="fsRoot" data-rv="1">
3083
+ <div id="fsRoot">
3110
3084
  <div class="fscard fsrail">
3111
3085
  <button class="railtg" id="fsRailTg" title="Fold the rail"></button><!-- panel icon filled by tgUpdate (v45 §4) -->
3112
3086
  <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 +6298,6 @@ function render(){
6324
6298
  // queueOrder); actions call the SAME endpoints (approve/dismiss/retry/reply…).
6325
6299
  // ============================================================================
6326
6300
  const fsUI = {
6327
- rv: 1, // approved screenshot-free B Status dot design; Review has one stable view
6328
6301
  band: Number(localStorage.getItem('fsBand') ?? 0), // theme-6 band choice
6329
6302
  revMore: false, // "+N more" fold on the review list (§1.5-8)
6330
6303
  todoFold: false, doneOpen: false, rejOpen: false, todoMore: false,
@@ -6341,7 +6314,16 @@ const FS_ICONS = {
6341
6314
  // per-project count icons (HANDOFF-v45 §10): To Do = rounded square, Review = Scan
6342
6315
  // (magnifier) — both stroked, NEUTRAL ink (never green); shape is the only distinction.
6343
6316
  const FS_ICO_TD = '<svg class="pcg" viewBox="0 0 24 24"><rect x="5" y="5" width="14" height="14" rx="3.5"/></svg>';
6344
- const FS_ICO_SCAN = '<svg class="pcg" viewBox="0 0 24 24"><circle cx="10.5" cy="10.5" r="5.5"/><path d="M19 19l-4.5-4.5"/></svg>';
6317
+ // Eye, not magnifying glass or checkmark (Masa 2026-07-26): the magnifying-glass
6318
+ // read as "search". The first replacement — a square with a checkmark inside,
6319
+ // matching FS_ICO_TD's (To Do) frame — read as "done" instead ("あれ、これだと
6320
+ // めっちゃdoneっぽいわ"): a checkmark is universally a completion signal, and
6321
+ // pairing it with To Do's exact square made it worse, not better. An eye carries
6322
+ // neither "search" nor "done" — it says "look at this", which is what Review
6323
+ // actually is. Standard minimal eye glyph (outline + pupil), legible at the real
6324
+ // 11px render size (verified: a smaller pupil or added detail merges into a
6325
+ // blob at that size — this is the simplest shape that still reads as an eye).
6326
+ const FS_ICO_SCAN = '<svg class="pcg" viewBox="0 0 24 24"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>';
6345
6327
  // hover-only Reply / Edit / Delete on a To Do row (§1.5-10; flagship ACTS port)
6346
6328
  function fsActs(goalId){
6347
6329
  if (goalId == null || !state.goals.some((g) => g.id === goalId)) return '';
@@ -6352,7 +6334,6 @@ function fsActs(goalId){
6352
6334
  function renderFlagship(){
6353
6335
  if (state.layout !== 'flagship') return;
6354
6336
  const root = $('fsRoot'); if (!root) return;
6355
- root.dataset.rv = String(fsUI.rv);
6356
6337
  // Wall-clock phase for the spinning markers: any ring re-created this render (the board
6357
6338
  // re-renders often on progress %) starts at the angle a continuous one would be at now,
6358
6339
  // instead of snapping back to 0deg — no "jump". The rail ring is guarded separately and
@@ -6933,8 +6914,10 @@ function renderFsReview(){
6933
6914
  const pgoals = state.goals.filter((g) => g.projectId === state.active);
6934
6915
  const list = state.tasks.filter((t) => t.projectId === state.active);
6935
6916
  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
6917
+ // Folded goals belong only to the carrier's Review dialog ("Also carries").
6918
+ // They are historical context, not independently reviewable work, so rendering
6919
+ // them here produced the contradictory "0 ready" header followed by stale cards.
6920
+ if (!rows.length) { el.innerHTML = ''; return; } // §1.5-3: no reviews → the UI itself does not exist
6938
6921
  const green = rows.filter((r) => !r.testResult || r.testResult.ok !== false).length;
6939
6922
  const blockedN = pgoals.filter((g) => g.status === 'blocked').length;
6940
6923
  const pct = rows.length ? Math.round((green / rows.length) * 100) : 0;
@@ -6974,13 +6957,6 @@ function renderFsReview(){
6974
6957
  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
6958
  <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
6959
  };
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
6960
  // V3-C (Masa決定 2026-07-07): no collapsed strip — it duplicated the sm2 meter right
6985
6961
  // above. Reviews render directly: meter + Review header + cards (+N more fold, §1.5-8).
6986
6962
  // The meter counts what is reviewable, so it only exists when something is (§1.5-3):
@@ -6991,7 +6967,7 @@ function renderFsReview(){
6991
6967
  <div class="m-bar"><i style="width:${pct}%"></i></div>
6992
6968
  <div class="s">${esc(sline)}</div></div>` : ''}
6993
6969
  <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('')}
6970
+ <div class="revlist">${shown.map(card).join('')}
6995
6971
  ${rows.length > RCAP ? `<div class="morefold" id="fsRevMore">${fsUI.revMore ? 'less' : `+${rows.length - RCAP} more`}<span class="g">›</span></div>` : ''}</div>`;
6996
6972
  // Mirror the ready-review count onto the mobile top-bar "Review N" opener (H23).
6997
6973
  const _hn = document.getElementById('fsHambN'); if (_hn) _hn.textContent = green > 0 ? String(green) : '';
@@ -7009,8 +6985,8 @@ function renderFsReview(){
7009
6985
  if (e.target.closest('a') || e.target.closest('.act') || e.target.closest('[data-goalreverify]')) return;
7010
6986
  openReviewChecklist(Number(c.dataset.fsrev));
7011
6987
  };
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).
6988
+ // Folded goals are shown, and restored, on the carrier's side in the See-all
6989
+ // "Also carries" section (saItemHtml + the seeall handler).
7014
6990
  for (const b of el.querySelectorAll('[data-fsapprove]')) b.onclick = async (e) => { e.stopPropagation();
7015
6991
  let ok = true, mergeGoal = null;
7016
6992
  for (const id of b.dataset.fsapprove.split(',')) {
@@ -7289,9 +7265,9 @@ function renderFsTodo(){
7289
7265
  ${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
7266
  ${(parkedRows || upRows) ? `<span class="tlcap"${(doingRows || paused) || needsRows || rejected.length ? '' : ' style="padding-top:0"'}>Next up</span>${parkedRows}${upRows}` : ''}
7291
7267
  ${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>
7268
+ <div class="qadd" data-fsnew="1" role="button" tabindex="0" title="New goal"><span class="paplus"></span><span>New</span></div>
7293
7269
  <span class="tlcap">Later</span>
7294
- <button class="qadd lateradd" data-fslaternew="1" title="New Later goal"><span class="paplus"></span>New</button>
7270
+ <div class="qadd lateradd" data-fslaternew="1" role="button" tabindex="0" title="New Later goal"><span class="paplus"></span><span>New</span></div>
7295
7271
  ${laterIdle}
7296
7272
  ${laterRows}
7297
7273
  </div>
@@ -7299,12 +7275,12 @@ function renderFsTodo(){
7299
7275
  <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
7276
  el.classList.toggle('folded', !!fsUI.todoFold);
7301
7277
  $('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();
7278
+ const qadd = el.querySelector('.qadd[data-fsnew]');
7279
+ if (qadd) qadd.onclick = (e) => { e.stopPropagation(); fsInlineNewRow(qadd, 'Add to To Do…', (text) => fsCreateGoal(text, false)); };
7304
7280
  const laterAdd = el.querySelector('.qadd[data-fslaternew]');
7305
7281
  if (laterAdd) laterAdd.onclick = (e) => {
7306
7282
  e.stopPropagation();
7307
- fsInlineInput(laterAdd, '', 'Add to Later…', fsCreatePendingGoal);
7283
+ fsInlineNewRow(laterAdd, 'Add to Later…', (text) => fsCreateGoal(text, true));
7308
7284
  };
7309
7285
  const dl = el.querySelector('#fsDoneLine');
7310
7286
  if (dl) dl.onclick = () => { fsUI.doneOpen = !fsUI.doneOpen; dl.classList.toggle('open', fsUI.doneOpen); el.querySelector('#fsDonelist').hidden = !fsUI.doneOpen; };
@@ -7381,19 +7357,50 @@ function renderFsTodo(){
7381
7357
  const r = await fetch(withKey(`/api/goals/${b.dataset.fsdel}`), { method: 'DELETE' });
7382
7358
  if (!r.ok) showErr('Cannot delete a running/finished goal — reply to append instead'); else await refresh(); };
7383
7359
  }
7384
- async function fsCreatePendingGoal(text){
7360
+ async function fsCreateGoal(text, pending){
7385
7361
  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 }) });
7362
+ body: JSON.stringify({ projectId: state.active, text, pr: 'auto', model: state.stickyModel, effort: state.stickyEffort, agent: state.connectedApp, mode: 'auto', pending }) });
7387
7363
  if (r.status === 402) {
7388
7364
  const data = await r.json().catch(() => ({}));
7389
7365
  showBillingBlocked(data.blocked);
7390
- return;
7366
+ return false;
7391
7367
  }
7392
- if (!r.ok) { showErr('Later add failed.'); return; }
7368
+ if (!r.ok) { showErr(pending ? 'Later add failed.' : 'To Do add failed.'); return false; }
7393
7369
  clearBillingBlocked();
7394
7370
  const goal = await r.json();
7395
7371
  if (goal?.id) upsert(state.goals, goal);
7396
7372
  render();
7373
+ return true;
7374
+ }
7375
+ function fsInlineNewRow(row, placeholder, onSubmit){
7376
+ if (!row) return;
7377
+ const current = row.querySelector('input');
7378
+ if (current) { current.focus(); return; }
7379
+ const label = row.querySelector('span:last-child');
7380
+ if (!label) return;
7381
+ const original = label.textContent;
7382
+ const inp = document.createElement('input');
7383
+ inp.type = 'text'; inp.placeholder = placeholder; inp.setAttribute('aria-label', placeholder);
7384
+ label.replaceWith(inp); row.classList.add('editing'); inp.focus();
7385
+ let done = false;
7386
+ const restore = () => {
7387
+ if (done) return;
7388
+ done = true; row.classList.remove('editing');
7389
+ if (inp.isConnected) { const span = document.createElement('span'); span.textContent = original; inp.replaceWith(span); }
7390
+ };
7391
+ inp.onkeydown = (e) => {
7392
+ if (e.key === 'Enter' && !e.isComposing) {
7393
+ e.preventDefault();
7394
+ const value = inp.value.trim();
7395
+ if (!value) return;
7396
+ done = true; row.classList.remove('editing'); inp.disabled = true;
7397
+ Promise.resolve(onSubmit(value)).then((ok) => {
7398
+ if (ok !== false || !inp.isConnected) return;
7399
+ done = false; row.classList.add('editing'); inp.disabled = false; inp.focus();
7400
+ });
7401
+ } else if (e.key === 'Escape') restore();
7402
+ };
7403
+ inp.onblur = () => setTimeout(restore, 150);
7397
7404
  }
7398
7405
  function fsInlineInput(afterEl, value, placeholder, onSubmit){
7399
7406
  if (!afterEl || afterEl.nextElementSibling?.classList.contains('fsinline')) return;
@@ -9185,6 +9192,15 @@ function saRowFromDigest(r, gs, ts){
9185
9192
  run: (() => {
9186
9193
  return latestGoalRunForOutcome(g?.id, ts, outcome);
9187
9194
  })(),
9195
+ // The worker DID try to declare something but it came out unusable (goal #733,
9196
+ // 2026-07-26: a Write followed by an Edit broke the JSON mid-edit; Review showed
9197
+ // no "Open it" section and no explanation, indistinguishable from "nothing to
9198
+ // run"). engine/server.mjs's readRunDeclaration sets task.runDeclError in that
9199
+ // case; only shown when there's no successful `run` to display instead.
9200
+ runDeclError: (() => {
9201
+ const t = ts.find((x) => r.goalIds.includes(x.goalId) && x.runDeclError);
9202
+ return t?.runDeclError ?? null;
9203
+ })(),
9188
9204
  // Explicit review-summary display fields (spec §11: the card's shape follows the
9189
9205
  // task). A goal's reviewSummary may carry them directly (e.g. a conversational task
9190
9206
  // that reports an Answer + its Activity, or a ticket whose test count / diff totals
@@ -9611,9 +9627,20 @@ function saItemHtml(it, i){
9611
9627
  // failure reason land after the press. Nothing declared → no section (§1.5-3). ──
9612
9628
  // it.run?.compare/title alone (no cmd/url) has nothing a press can start — a
9613
9629
  // compare-only or title-only declaration must not draw the "Start it" button.
9630
+ // it.runDeclError (2026-07-26, goal #733): the worker DID try to declare an
9631
+ // artifact/command, but .manager-run.json came out unusable (e.g. a Write then
9632
+ // an Edit broke the JSON mid-edit) — this must not look identical to "nothing
9633
+ // to run" (§1.5-3 no empty box still applies: don't invent a fake Open it, but
9634
+ // don't stay silent about a real attempt either). Same honest-advisory shape as
9635
+ // prRepairBody below. A reply is the repair path: the worker rewrites the file.
9636
+ const runDeclErrorBody = (!((it.run?.cmd || it.run?.url)) && it.runDeclError)
9637
+ ? `<div class="ropen rdeclerr"><p class="rbody v">${failureLang() === 'ja' ? 'AIが開けるものを用意しようとしましたが、記録が読み取れませんでした。返信して直させてください。' : 'The AI tried to declare something to open here, but the record was unreadable. Reply to have it rewritten.'}</p></div>`
9638
+ : '';
9614
9639
  const openBody = (it.run?.cmd || it.run?.url)
9615
9640
  ? `<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
- : '';
9641
+ : it.pr
9642
+ ? `<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>`
9643
+ : runDeclErrorBody;
9617
9644
  const prRepairBody = it.goal?.prError
9618
9645
  ? `<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
9646
  : '';
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;
@@ -3120,8 +3151,21 @@ export function workerPrompt(task, goal, lastFailure, agentName = 'claude-code',
3120
3151
  // (research, summaries, answers) simply skip this — the worker decides,
3121
3152
  // never us.
3122
3153
  `- 人が実際に動かして結果を見られる依頼なら、最後にプロジェクト直下へ ${RUN_DECL_FILE} を書く: {"cmd": "<起動コマンド>", "url": "<開くURL>", "note": "<何が見えるか1行>", "check": "<人に何を見て判断してほしいか1行>", "title": "<この作業のチケット名。対象が分かる程度に説明的で動作を含む短い名前。例「看板モードの追加」「ヘッダのズレの修正」。依頼文をそのまま写さない>"}。Manager はこれを使って人に「動かして見る」と「何を見ればいいか」を出す。動かして見せるものが無い依頼(調査・要約・質問への回答など)では書かなくてよい。`,
3154
+ // Root-cause prevention (goal #733, 2026-07-26): the worker Wrote this file,
3155
+ // then Edit'd it, and the edit broke the JSON mid-edit — Manager silently
3156
+ // dropped the whole declaration (fixed separately: it now surfaces an honest
3157
+ // note instead of vanishing). But the honest note is a recovery, not a fix —
3158
+ // the real fix is stopping the corruption before it happens. Edit requires an
3159
+ // exact byte-for-byte match against the file's current content; a small slip
3160
+ // (a stray character, a stale in-memory copy) breaks the JSON with no error
3161
+ // the worker would notice, because it happens in the tool call, not in
3162
+ // anything it reads back. Write overwrites the whole file atomically — there
3163
+ // is no "current content" to mismatch against — so telling the worker to
3164
+ // always fully overwrite rather than partially edit removes the failure mode
3165
+ // structurally, not just its symptom.
3166
+ `- ${RUN_DECL_FILE} は毎回 Write で全文を書き直すこと。Edit で一部だけ直さない(既存内容と一致しないと壊れたJSONになり、Managerに握りつぶされて人に何も表示されない)。`,
3123
3167
  `- Markdown・設計文書・調査メモなど文章そのものが成果物なら、${RUN_DECL_FILE} に別のHTMLや過去のURLを書かない。最終回答に成果物の本文も貼り、保存先のMarkdownリンクを添えること。「ファイルに書きました」だけで終わらない。`,
3124
- `- 「ローカルで見れない」というフィードバックには、ターミナル操作や npx の再実行を案内して終わらないこと。${RUN_DECL_FILE} を実際に開ける内容へ更新し、可能なら自分でURLへ接続確認してから完了すること。`,
3168
+ `- 「ローカルで見れない」というフィードバックには、ターミナル操作や npx の再実行を案内して終わらないこと。${RUN_DECL_FILE} を Write で全文書き直して実際に開ける内容へ更新し、可能なら自分でURLへ接続確認してから完了すること。`,
3125
3169
  // Links are rendered as links. The product cannot tell a real one from a
3126
3170
  // decorative one, so the only place this can be held is here.
3127
3171
  `- 自分が作っていない場所へのリンクを報告に書かない(存在しないURLや「詳しくはここ」のような飾りのリンクは、そのままリンクとして人に表示される)。`,
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);
@@ -2425,7 +2407,18 @@ function readRunDeclaration(task, workDir) {
2425
2407
  try { unlinkSync(file); } catch { /* best effort — never fail a task over cleanup */ }
2426
2408
  const run = parseRunDeclaration(raw);
2427
2409
  if (!run) {
2428
- sendAct(task, `run declaration ignored ${RUN_DECL_FILE} was not usable JSON`);
2410
+ // The worker DID try to declare something (the file exists) but it came out
2411
+ // unusable — e.g. a Write followed by an Edit that broke the JSON mid-edit
2412
+ // (goal #733, 2026-07-26: Masa saw a working artifact with no "Open it" and
2413
+ // no explanation at all — the Review card just silently had no section,
2414
+ // indistinguishable from "nothing to run"). Silence was already the honest
2415
+ // answer for a request with nothing to run; it stops being honest the moment
2416
+ // a declaration was attempted and swallowed. Keep a short snippet of what was
2417
+ // actually written (raw content is deleted above and never otherwise
2418
+ // recoverable) so both the activity log and the Review card can say WHY,
2419
+ // instead of the human having to guess or dig through file loss.
2420
+ task.runDeclError = { snippet: String(raw ?? '').slice(0, 200) };
2421
+ sendAct(task, `run declaration ignored — ${RUN_DECL_FILE} was not usable JSON: ${task.runDeclError.snippet.slice(0, 80)}`);
2429
2422
  return;
2430
2423
  }
2431
2424
  task.run = { ...run, dir: workDir };
@@ -2553,10 +2546,11 @@ async function startPreview(task) {
2553
2546
  if (managerPackage && existsSync(join(dir, 'engine', 'server.mjs')) && existsSync(join(dir, 'app', 'index.html'))) {
2554
2547
  const eph = await startEphemeralApp(dir);
2555
2548
  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 };
2549
+ const previewUrl = rebasePreviewUrl(url, eph.url);
2550
+ const entry = { proc: eph.child, url: previewUrl, expires: Date.now() + PREVIEW_IDLE_MS };
2557
2551
  previews.set(task.id, entry);
2558
2552
  eph.child.on('close', () => { if (previews.get(task.id) === entry) previews.delete(task.id); });
2559
- return { url: eph.url, reused: false, started: true };
2553
+ return { url: previewUrl, reused: false, started: true };
2560
2554
  }
2561
2555
  // Declared a URL but no command: the thing is already reachable (an app the
2562
2556
  // worker left running, a hosted page). Nothing to start.
@@ -3279,11 +3273,10 @@ async function finishGoalIfComplete(goal, project, activityTask = null) {
3279
3273
  // (e.g. the skip endpoint) still PR from the isolated worktree, not the shared dir.
3280
3274
  const wdir = (goalWorkDirs.get(goal.id) && existsSync(goalWorkDirs.get(goal.id))) ? goalWorkDirs.get(goal.id) : project.dir;
3281
3275
  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
3276
+ // Close-out always delegates to createGoalPR so it can record an explicit
3277
+ // No PR decision as well as create/update a requested PR. `goal.wantsPR` is
3278
+ // authoritative: changed files alone must never override the user's choice.
3279
+ // Always call it (not just `if (!goal.pr)`): when a goal already has a PR — e.g. a review
3287
3280
  // comment sent it back for rework and the reply task just finished — this is
3288
3281
  // the only place that pushes the rework diff, and createGoalPR itself decides
3289
3282
  // "new PR" vs "push onto the existing PR's branch" from goal.pr/goal.prBranch.
@@ -3626,13 +3619,16 @@ async function createGoalPR(goal, project, goalTasks, reviewDefinition = DEFAULT
3626
3619
  // the diff. Proof artifacts (.manager-proof) are intentional and kept.
3627
3620
  const SCRATCH = /(^|\/)\.tmp|\.log$|(^|\/)\.manager-wt(\/|$)|(^|\/)node_modules(\/|$)|(^|\/)secret\.key$/;
3628
3621
  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; }
3622
+ if (!shouldCreateGoalPR(goal, files)) {
3623
+ goal.pr = null;
3624
+ goal.prBranch = null;
3625
+ goal.prError = null;
3626
+ saveGoal(goal);
3627
+ goalAct(goal.wantsPR === false
3628
+ ? 'Deliverable is No PR, so PR creation is skipped even though files changed.'
3629
+ : 'No code changes detected, so PR creation is skipped.');
3630
+ return;
3631
+ }
3636
3632
  const unsafe = prSafetyIssue(goal, project, files);
3637
3633
  if (unsafe) {
3638
3634
  goal.prError = unsafe.message.slice(0, 300);
@@ -4155,7 +4151,7 @@ const server = createServer(async (req, res) => {
4155
4151
  req.on('data', (d) => { body += d; });
4156
4152
  req.on('end', async () => {
4157
4153
  try {
4158
- const { projectId, text, pr, images, model, effort, source, pending, review, note, mode, skill, agent, worktree, workspaceMode } = JSON.parse(body);
4154
+ const { projectId, text, pr, existingPr, images, model, effort, source, pending, review, note, mode, skill, agent, worktree, workspaceMode } = JSON.parse(body);
4159
4155
  const project = projects.find((p) => p.id === projectId);
4160
4156
  if (!project || !text?.trim()) return json(res, 400, { error: 'projectId and text required' });
4161
4157
  // Chat-driven play/pause (Masa: "チャットで再生一時停止はおせる"). Deterministic
@@ -4269,16 +4265,20 @@ const server = createServer(async (req, res) => {
4269
4265
  // (connected-agents Phase A) — a requested/default agent that isn't
4270
4266
  // present would otherwise spawn a worker doomed to fail at launch.
4271
4267
  const goalAgent = pickAvailableAgent(workerAgent(agent), AVAILABLE_AGENTS);
4272
- const priorFailureMemory = recentProjectFailureMemory(projectId, text.trim());
4268
+ // Failure memory belongs to one conversation. A new Goal must not
4269
+ // inherit an unrelated Goal's old failure just because both mention UI.
4270
+ const priorFailureMemory = [];
4273
4271
  const resolvedWorkspaceMode = resolveWorkspaceMode(workspaceMode ?? worktree);
4274
4272
  const workspaceDirty = resolvedWorkspaceMode === 'direct-workspace' ? projectDirtyInfo(project.dir) : {};
4273
+ const importedPr = review ? normalizePullRequestUrl(existingPr) : null;
4274
+ if (review && existingPr && !importedPr) return json(res, 400, { error: 'existingPr must be a GitHub pull request URL' });
4275
4275
  const goal = {
4276
4276
  id: nextId++, projectId, text: text.trim().slice(0, 8000),
4277
4277
  status: (needsFolder || needsReviewPref) ? 'needsInput' : review ? 'review' : pending ? 'pending' : 'stacked',
4278
4278
  question: folderQuestion || reviewPrefQuestion,
4279
4279
  reviewOnly: review ? true : undefined,
4280
4280
  note: review && typeof note === 'string' && note.trim() ? note.trim().slice(0, 8000) : undefined,
4281
- wantsPR: review ? false : resolveWantsPR(pr, text, getReviewDefinition(projectId).defaultWantsPR),
4281
+ wantsPR: review ? Boolean(importedPr) : resolveWantsPR(pr, text, getReviewDefinition(projectId).defaultWantsPR),
4282
4282
  model: workerModel(goalAgent, model),
4283
4283
  effort: workerEffort(goalAgent, effort),
4284
4284
  agent: goalAgent,
@@ -4305,7 +4305,7 @@ const server = createServer(async (req, res) => {
4305
4305
  previousSession: false,
4306
4306
  }),
4307
4307
  priorFailureMemory: priorFailureMemory.length ? priorFailureMemory : undefined,
4308
- plan: null, pr: undefined, createdAt: new Date().toISOString(),
4308
+ plan: null, pr: importedPr || undefined, createdAt: new Date().toISOString(),
4309
4309
  // Stage 2 (multi-user): who this goal belongs to — 'owner' for
4310
4310
  // key-auth/MANAGER_OWNER_EMAIL, else the creator's Google email.
4311
4311
  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.72",
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": {