@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 +55 -61
- package/engine/lib.mjs +33 -2
- package/engine/server.mjs +26 -37
- package/package.json +1 -1
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"
|
|
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
|
-
|
|
6937
|
-
|
|
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('')}
|
|
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
|
|
7013
|
-
//
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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]');
|
|
7303
|
-
if (qadd) qadd.onclick = () =>
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
//
|
|
3283
|
-
//
|
|
3284
|
-
//
|
|
3285
|
-
// (
|
|
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
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
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
|
-
|
|
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 ?
|
|
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