@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 +89 -62
- package/engine/lib.mjs +47 -3
- package/engine/server.mjs +38 -38
- 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)}
|
|
@@ -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"
|
|
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
|
-
|
|
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
|
-
|
|
6937
|
-
|
|
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('')}
|
|
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
|
|
7013
|
-
//
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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]');
|
|
7303
|
-
if (qadd) qadd.onclick = () =>
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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;
|
|
@@ -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}
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
//
|
|
3283
|
-
//
|
|
3284
|
-
//
|
|
3285
|
-
// (
|
|
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
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
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
|
-
|
|
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 ?
|
|
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