@galda/cli 0.10.58 → 0.10.64
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 +196 -36
- package/engine/lib.mjs +19 -0
- package/engine/server.mjs +69 -10
- package/engine/worker-availability.mjs +4 -0
- package/package.json +1 -1
package/app/index.html
CHANGED
|
@@ -2020,6 +2020,12 @@
|
|
|
2020
2020
|
.sa-item.lg .sa-act{margin-top:13px;border-top:1px solid var(--hair);padding-top:13px}
|
|
2021
2021
|
.sa-chatwrap{flex:1;min-width:150px;position:relative;display:flex;align-items:center}
|
|
2022
2022
|
.sa-chatwrap .sa-chat{padding-right:42px}
|
|
2023
|
+
.sa-replybox{flex:1;min-width:150px;display:flex;flex-direction:column;gap:6px}
|
|
2024
|
+
.sa-replyattach{display:none;gap:6px;flex-wrap:wrap;min-height:0}
|
|
2025
|
+
.sa-replyattach.has{display:flex}
|
|
2026
|
+
.sa-replythumb{position:relative;width:44px;height:44px;border:1px solid var(--hair2);border-radius:7px;overflow:hidden;background:var(--canvas)}
|
|
2027
|
+
.sa-replythumb img{width:100%;height:100%;object-fit:cover;display:block}
|
|
2028
|
+
.sa-replythumb button{position:absolute;right:2px;top:2px;width:17px;height:17px;border:0;border-radius:50%;background:rgba(0,0,0,.72);color:#fff;font:500 12px/17px var(--ui);padding:0;cursor:pointer}
|
|
2023
2029
|
/* send-back button = the spinning prism ring (§1.5-6), same palette source as the
|
|
2024
2030
|
main composer's --ringgrad; centre stays transparent so it sits into the row */
|
|
2025
2031
|
@keyframes iringspin{to{transform:rotate(360deg)}}
|
|
@@ -2227,6 +2233,19 @@
|
|
|
2227
2233
|
.redesign .ghfoot{margin-top:8px;font-family:var(--mono);font-size:11.5px;color:var(--ink3);display:flex;gap:2px;align-items:center;flex-wrap:wrap}
|
|
2228
2234
|
.redesign .ghfoot .ghopen{color:var(--ink2);text-decoration:none}
|
|
2229
2235
|
.redesign .ghfoot .ghopen:hover{color:var(--blue)}
|
|
2236
|
+
/* Keep the useful summary visible, but fold long diffs so the reply box stays nearby. */
|
|
2237
|
+
.redesign .ghwrap{position:relative}
|
|
2238
|
+
.redesign .ghwrap.clip .rdiff2{max-height:264px}
|
|
2239
|
+
.redesign .ghfade{display:none;position:absolute;left:0;right:0;bottom:0;height:52px;pointer-events:none;
|
|
2240
|
+
border-radius:0 0 8px 8px;background:linear-gradient(to bottom,rgba(0,0,0,0),var(--panel))}
|
|
2241
|
+
.redesign .ghwrap.clip .ghfade{display:block}
|
|
2242
|
+
.redesign .ghmore{margin-top:8px;display:inline-flex;align-items:center;gap:7px;font-family:var(--mono);
|
|
2243
|
+
font-size:11.5px;color:var(--ink3);background:transparent;border:0;padding:5px 9px 5px 7px;border-radius:8px;cursor:pointer}
|
|
2244
|
+
.redesign .ghmore:hover{background:var(--hover);color:var(--ink)}
|
|
2245
|
+
.redesign .ghmore .chev{width:11px;height:11px;flex:0 0 auto;transition:transform .16s}
|
|
2246
|
+
.redesign .ghmore .chev path{stroke:currentColor;stroke-width:1.8;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
2247
|
+
.redesign .ghmore.open .chev{transform:rotate(180deg)}
|
|
2248
|
+
.redesign .ghmore .n{color:var(--ink2);font-variant-numeric:tabular-nums}
|
|
2230
2249
|
/* meta links carry NO underline — PR is a real <a href> (spec §5, Masa 2026-07-20) */
|
|
2231
2250
|
.redesign .rmeta a{text-decoration:none}
|
|
2232
2251
|
/* Request / What-to-check = full contrast, same ink as the title (spec §2, no fade) */
|
|
@@ -7055,9 +7074,10 @@ function renderFsTodo(){
|
|
|
7055
7074
|
for (const b of el.querySelectorAll('[data-fslog]')) b.onclick = () => fsToggleLog(Number(b.dataset.fslog));
|
|
7056
7075
|
for (const b of el.querySelectorAll('[data-fsglog]')) b.onclick = () => toggleGoalDetail(Number(b.dataset.fsglog));
|
|
7057
7076
|
for (const b of el.querySelectorAll('[data-fsreverify]')) b.onclick = async () => {
|
|
7058
|
-
b.disabled = true; b.textContent = '
|
|
7077
|
+
b.disabled = true; b.textContent = 'Starting…';
|
|
7059
7078
|
const r = await fetch(withKey(`/api/goals/${b.dataset.fsreverify}/reverify`), { method: 'POST' });
|
|
7060
|
-
if (!r.ok) showErr('
|
|
7079
|
+
if (!r.ok) { showErr(document.documentElement.lang === 'ja' ? 'Retryに失敗しました' : 'Retry failed.'); b.disabled = false; b.textContent = 'Retry'; }
|
|
7080
|
+
else await refresh();
|
|
7061
7081
|
};
|
|
7062
7082
|
for (const b of el.querySelectorAll('[data-fsarchive]')) b.onclick = async () => {
|
|
7063
7083
|
const r = await fetch(withKey(`/api/goals/${b.dataset.fsarchive}/archive`), { method: 'POST' });
|
|
@@ -7693,29 +7713,66 @@ function showBillingBlocked(blocked){
|
|
|
7693
7713
|
billingBlockedActive = true;
|
|
7694
7714
|
}
|
|
7695
7715
|
|
|
7696
|
-
|
|
7697
|
-
|
|
7698
|
-
|
|
7699
|
-
|
|
7700
|
-
|
|
7701
|
-
|
|
7702
|
-
|
|
7703
|
-
|
|
7704
|
-
|
|
7705
|
-
'bad-credentials': 'Worker sign-in failed. Check ANTHROPIC_API_KEY, or run `claude` and /login, then relaunch the manager from that terminal.',
|
|
7706
|
-
'probe-failed': 'Couldn’t verify claude sign-in. Ensure the `claude` CLI is installed and signed in, then relaunch the manager.',
|
|
7707
|
-
'rate-limited': 'Claude is temporarily unavailable. Your Claude tasks are safely waiting. Tap “Run with Codex” beside a task to start it now.',
|
|
7716
|
+
const AUTH_COPY = {
|
|
7717
|
+
ja: {
|
|
7718
|
+
connectTitle: '接続が必要です', connectBody: 'Claude Codeとの接続が切れています。ボタンから接続してください。', connect: 'Claude Codeを接続', retry: '接続を確認',
|
|
7719
|
+
limitTitle: 'Claudeは利用上限です', limitBody: '作業は安全に待機しています。すぐ続ける場合は、待機中のタスクをCodexへ切り替えられます。', limitWait: '作業は安全に待機しています。Claudeが再び使えるようになると自動で再開します。', codex: 'Codexで続ける',
|
|
7720
|
+
},
|
|
7721
|
+
en: {
|
|
7722
|
+
connectTitle: 'Connect Claude Code', connectBody: 'Claude Code is disconnected. Use the button to reconnect and continue.', connect: 'Connect Claude Code', retry: 'Check connection',
|
|
7723
|
+
limitTitle: 'Claude limit reached', limitBody: 'Your work is safely waiting. Switch the waiting tasks to Codex if you want to continue now.', limitWait: 'Your work is safely waiting and will resume automatically when Claude is available.', codex: 'Continue with Codex',
|
|
7724
|
+
},
|
|
7708
7725
|
};
|
|
7709
7726
|
function renderAuthBanner(auth){
|
|
7710
7727
|
const bar = $('errbar');
|
|
7711
7728
|
if (auth && auth.ok === false){
|
|
7712
7729
|
authDown = true;
|
|
7713
|
-
const
|
|
7730
|
+
const copy = AUTH_COPY[billingLang()];
|
|
7731
|
+
const limited = auth.reason === 'rate-limited';
|
|
7732
|
+
const canUseCodex = (state.agents || []).includes('codex');
|
|
7714
7733
|
bar.classList.add('billing'); // strips #errbar's error fill so the card stands alone
|
|
7715
|
-
bar.innerHTML = `<div class="bx-ask"><div class="bx-qt"><div class="bx-lb"
|
|
7716
|
-
+ `<div class="bx-q">${
|
|
7734
|
+
bar.innerHTML = `<div class="bx-ask${limited ? ' info' : ''}"><div class="bx-qt"><div class="bx-lb">${limited ? copy.limitTitle : copy.connectTitle}</div>`
|
|
7735
|
+
+ `<div class="bx-q">${limited ? (canUseCodex ? copy.limitBody : copy.limitWait) : copy.connectBody}</div></div>`
|
|
7736
|
+
+ `<div class="bx-act">${limited && canUseCodex
|
|
7737
|
+
? `<button class="bx-cta" id="bxUseCodex">${copy.codex}</button>`
|
|
7738
|
+
: limited ? '' : `<button class="bx-cta" id="bxConnectClaude">${copy.connect}</button><button class="bx-cta" id="bxRetryClaude">${copy.retry}</button>`}</div></div>`;
|
|
7717
7739
|
bar.style.display = 'block';
|
|
7718
7740
|
clearTimeout(errTimer); // this banner persists — no auto-hide
|
|
7741
|
+
const codex = bar.querySelector('#bxUseCodex');
|
|
7742
|
+
if (codex) codex.onclick = async () => {
|
|
7743
|
+
codex.disabled = true;
|
|
7744
|
+
try {
|
|
7745
|
+
const r = await fetch(withKey('/api/worker-auth/run-with-codex'), { method: 'POST' });
|
|
7746
|
+
const body = await r.json().catch(() => ({}));
|
|
7747
|
+
if (!r.ok) throw new Error(body.error || 'Could not switch to Codex.');
|
|
7748
|
+
renderAuthBanner({ ok: true });
|
|
7749
|
+
showToast(copy === AUTH_COPY.ja ? `${body.changed}件をCodexで再開しました。` : `${body.changed} task${body.changed === 1 ? '' : 's'} resumed with Codex.`);
|
|
7750
|
+
await refresh();
|
|
7751
|
+
} catch (e) { showErr(e.message || 'Could not switch to Codex.'); }
|
|
7752
|
+
finally { codex.disabled = false; }
|
|
7753
|
+
};
|
|
7754
|
+
const connect = bar.querySelector('#bxConnectClaude');
|
|
7755
|
+
if (connect) connect.onclick = async () => {
|
|
7756
|
+
connect.disabled = true;
|
|
7757
|
+
try {
|
|
7758
|
+
const r = await fetch(withKey('/api/worker-auth/connect'), { method: 'POST' });
|
|
7759
|
+
if (!r.ok) throw new Error();
|
|
7760
|
+
showToast(copy === AUTH_COPY.ja ? '接続画面を開きました。完了後に「接続を確認」を押してください。' : 'Connection opened. When finished, choose “Check connection”.');
|
|
7761
|
+
} catch { showErr(copy === AUTH_COPY.ja ? '接続画面を開けませんでした。' : 'Could not open the connection.'); }
|
|
7762
|
+
finally { connect.disabled = false; }
|
|
7763
|
+
};
|
|
7764
|
+
const retry = bar.querySelector('#bxRetryClaude');
|
|
7765
|
+
if (retry) retry.onclick = async () => {
|
|
7766
|
+
retry.disabled = true;
|
|
7767
|
+
try {
|
|
7768
|
+
const r = await fetch(withKey('/api/worker-auth/retry'), { method: 'POST' });
|
|
7769
|
+
const body = await r.json().catch(() => ({}));
|
|
7770
|
+
if (!r.ok) throw new Error();
|
|
7771
|
+
renderAuthBanner(body.auth);
|
|
7772
|
+
if (body.auth?.ok) showToast(copy === AUTH_COPY.ja ? '接続できました。' : 'Connected.');
|
|
7773
|
+
} catch { showErr(copy === AUTH_COPY.ja ? 'まだ接続できていません。' : 'Not connected yet.'); }
|
|
7774
|
+
finally { retry.disabled = false; }
|
|
7775
|
+
};
|
|
7719
7776
|
} else if (authDown) {
|
|
7720
7777
|
authDown = false;
|
|
7721
7778
|
bar.classList.remove('billing');
|
|
@@ -8699,7 +8756,23 @@ function closeSummaryPera(){ $('summaryOverlay').classList.remove('show'); }
|
|
|
8699
8756
|
// 4 archived / 5 reverted — session-local; the server round-trip is what
|
|
8700
8757
|
// settles it (see saSync's "returned to review" reset).
|
|
8701
8758
|
// ============================================================================
|
|
8702
|
-
const SA = { open: false, items: [], diffs: {}, actLog: {}, msgs: {}, convOpen: new Set(), shotBox: {}, detOpen: new Set(), focusGid: null, focusScrolled: false, focusTimer: null, cur: null, solo: null };
|
|
8759
|
+
const SA = { open: false, items: [], diffs: {}, actLog: {}, msgs: {}, convOpen: new Set(), shotBox: {}, detOpen: new Set(), diffOpen: new Set(), attachments: {}, focusGid: null, focusScrolled: false, focusTimer: null, cur: null, solo: null };
|
|
8760
|
+
function saReplyAttachmentsHtml(goalId){
|
|
8761
|
+
return (SA.attachments[goalId] || []).map((a, i) => `<span class="sa-replythumb"><img src="${esc(a.url)}" alt="Attached image"><button type="button" data-saarm="${goalId}:${i}" aria-label="Remove image">×</button></span>`).join('');
|
|
8762
|
+
}
|
|
8763
|
+
function saPaintAttachments(goalId){
|
|
8764
|
+
const host = document.querySelector(`#seeall [data-saattach="${goalId}"]`);
|
|
8765
|
+
if (!host) return;
|
|
8766
|
+
host.innerHTML = saReplyAttachmentsHtml(goalId);
|
|
8767
|
+
host.classList.toggle('has', Boolean(SA.attachments[goalId]?.length));
|
|
8768
|
+
}
|
|
8769
|
+
async function saAttachImage(goalId, file){
|
|
8770
|
+
if (!file || !String(file.type).startsWith('image/')) return;
|
|
8771
|
+
const uploaded = await uploadReturn(file);
|
|
8772
|
+
if (!uploaded) return;
|
|
8773
|
+
(SA.attachments[goalId] ||= []).push(uploaded);
|
|
8774
|
+
saPaintAttachments(goalId);
|
|
8775
|
+
}
|
|
8703
8776
|
window.SA = SA; // top-level const isn't a window property; external checks probe window.SA (same convention as window.state above)
|
|
8704
8777
|
const SA_SVG = {
|
|
8705
8778
|
ok: '<svg viewBox="0 0 24 24"><path d="M4 12l5 5L20 6"/></svg>',
|
|
@@ -8981,13 +9054,13 @@ function saMarkWordSpans(rows){
|
|
|
8981
9054
|
}
|
|
8982
9055
|
return rows;
|
|
8983
9056
|
}
|
|
8984
|
-
function saGhDiffHtml(diff, pr){
|
|
9057
|
+
function saGhDiffHtml(diff, pr, goalId){
|
|
8985
9058
|
const rows = saMarkWordSpans(saUnifiedToGh(diff));
|
|
8986
9059
|
if (!rows.length) return '';
|
|
8987
9060
|
// Say what was left out. Before this, a 5000-line change rendered its first 80 rows
|
|
8988
9061
|
// and stopped — a quiet lie, and the one thing SPEC §7 names as forbidden.
|
|
8989
9062
|
const more = Math.max(0, saDiffRowCount(diff) - rows.length);
|
|
8990
|
-
return saGhDiffRowsHtml(rows, more, pr || '');
|
|
9063
|
+
return saGhDiffRowsHtml(rows, more, pr || '', goalId);
|
|
8991
9064
|
}
|
|
8992
9065
|
// One Activity transcript row (spec §3): You / tool (mono, bold name) / res (green) /
|
|
8993
9066
|
// Claude. Mirrors the flagship artifact's tx mapping so the pulldown pixel-matches.
|
|
@@ -9066,9 +9139,10 @@ function saMsgRowsHtml(msgs, expanded){
|
|
|
9066
9139
|
// word-level highlight (h) and, for a huge change, a "…+N 行 · GitHub で開く ↗"
|
|
9067
9140
|
// footer (only linking to GitHub when a PR exists). Mirrors the flagship artifact's
|
|
9068
9141
|
// ghDiffHtml so the app pixel-matches. `more` = extra unshown line count; `pr` = url.
|
|
9069
|
-
|
|
9142
|
+
const SA_DIFF_FOLD_OVER = 14;
|
|
9143
|
+
function saGhDiffRowsHtml(rows, more, pr, goalId){
|
|
9070
9144
|
if (!rows || !rows.length) return '';
|
|
9071
|
-
const
|
|
9145
|
+
const inner = rows.map((l) => {
|
|
9072
9146
|
const cls = l.t === 'add' ? 'gh-add' : l.t === 'del' ? 'gh-del' : 'gh-ctx';
|
|
9073
9147
|
const mk = l.t === 'add' ? '+' : l.t === 'del' ? '−' : '';
|
|
9074
9148
|
let code = esc(l.c);
|
|
@@ -9079,9 +9153,17 @@ function saGhDiffRowsHtml(rows, more, pr){
|
|
|
9079
9153
|
code = `${esc(l.c.slice(0, a))}<span class="gh-hi">${esc(l.c.slice(a, b))}</span>${esc(l.c.slice(b))}`;
|
|
9080
9154
|
} else if (l.h) { const h = esc(l.h); code = code.replace(h, `<span class="gh-hi">${h}</span>`); }
|
|
9081
9155
|
return `<div class="ghl ${cls}"><span class="gho">${l.o ?? ''}</span><span class="ghn">${l.n ?? ''}</span><span class="ghm">${mk}</span><span class="ghc">${code}</span></div>`;
|
|
9082
|
-
}).join('')
|
|
9156
|
+
}).join('');
|
|
9083
9157
|
const foot = more ? `<div class="ghfoot"><span>…+${more} 行</span>${pr ? ` · <a class="ghopen" href="${esc(pr)}" target="_blank" rel="noopener">GitHub で開く ↗</a>` : ''}</div>` : '';
|
|
9084
|
-
|
|
9158
|
+
if (goalId != null && rows.length > SA_DIFF_FOLD_OVER) {
|
|
9159
|
+
const open = SA.diffOpen.has(goalId);
|
|
9160
|
+
const label = open ? 'Collapse' : `Show all <span class="n">${rows.length}</span> lines`;
|
|
9161
|
+
return `<div class="ghwrap${open ? '' : ' clip'}"><div class="rdiff2">${inner}</div><div class="ghfade"></div></div>`
|
|
9162
|
+
+ `<button type="button" class="ghmore${open ? ' open' : ''}" data-ghmore="${goalId}" data-total="${rows.length}" aria-expanded="${open}">`
|
|
9163
|
+
+ `<svg class="chev" viewBox="0 0 12 12" aria-hidden="true"><path d="M2.5 4.5 6 8l3.5-3.5"/></svg><span class="ghlabel">${label}</span></button>`
|
|
9164
|
+
+ foot;
|
|
9165
|
+
}
|
|
9166
|
+
return `<div class="rdiff2">${inner}</div>` + foot;
|
|
9085
9167
|
}
|
|
9086
9168
|
// Screen-recording player for Proof (REVIEW-CARD-SPEC §6). A real file (recordingUrl)
|
|
9087
9169
|
// renders a native <video controls>; otherwise a self-contained SVG player mockup
|
|
@@ -9286,8 +9368,8 @@ function saItemHtml(it, i){
|
|
|
9286
9368
|
: (d && d.files.length) ? `<div class="rfiles">${d.files.map((f) => `${esc(f.path)} <i class="a">+${f.add}</i> <i class="d">−${f.del}</i>`).join('<br>')}</div>`
|
|
9287
9369
|
: (clientDiff && filePaths.length) ? `<div class="rfiles">${filePaths.map((p) => esc(p)).join('<br>')}</div>` : '';
|
|
9288
9370
|
const diffHtml = it.ghRows
|
|
9289
|
-
? saGhDiffRowsHtml(it.ghRows, it.diffTotal || 0, it.pr || '')
|
|
9290
|
-
: hasGitDiff ? saGhDiffHtml(d.diff, it.pr) : clientDiff ? saGhDiffHtml(clientDiff, it.pr) : '';
|
|
9371
|
+
? saGhDiffRowsHtml(it.ghRows, it.diffTotal || 0, it.pr || '', gk)
|
|
9372
|
+
: hasGitDiff ? saGhDiffHtml(d.diff, it.pr, gk) : clientDiff ? saGhDiffHtml(clientDiff, it.pr, gk) : '';
|
|
9291
9373
|
const changedBody = isConv ? '' : (changedDescHtml + filesListHtml + diffHtml);
|
|
9292
9374
|
// ── sensitive-area note (real: classifyChangeRisk forced the pause) ──
|
|
9293
9375
|
const risk = it.risk;
|
|
@@ -9386,7 +9468,7 @@ function saItemHtml(it, i){
|
|
|
9386
9468
|
${conversationSec}
|
|
9387
9469
|
${activitySec}
|
|
9388
9470
|
${S('Workflow', saWorkflowHtml(it))}
|
|
9389
|
-
<div class="sa-act">${attn ? '' : `<button class="sa-app" data-saact="app:${i}">${SA_SVG.ok}Approve</button><button class="sa-dis" data-saact="dis:${i}">Dismiss</button>`}<div class="sa-chatwrap"><input class="sa-chat" data-i="${i}" data-goal="${it.goalId}" placeholder="Reply, ask, or
|
|
9471
|
+
<div class="sa-act">${attn ? '' : `<button class="sa-app" data-saact="app:${i}">${SA_SVG.ok}Approve</button><button class="sa-dis" data-saact="dis:${i}">Dismiss</button>`}<div class="sa-replybox"><div class="sa-replyattach${SA.attachments[it.goalId]?.length ? ' has' : ''}" data-saattach="${it.goalId}">${saReplyAttachmentsHtml(it.goalId)}</div><div class="sa-chatwrap"><input class="sa-chat" data-i="${i}" data-goal="${it.goalId}" placeholder="Reply, ask, or paste an image…"><button class="sa-send" data-saact="send:${i}" title="Send"><svg viewBox="0 0 24 24"><path d="M12 19V5M5 12l7-7 7 7"/></svg></button></div></div><button class="sa-ic" title="Archive" data-saact="arch:${i}">${SA_SVG.arch}</button><button class="sa-ic" title="Revert" data-saact="rev:${i}">${SA_SVG.trash}</button></div>
|
|
9390
9472
|
</div>`;
|
|
9391
9473
|
}
|
|
9392
9474
|
// Scroll the See-all body so a card's top edge (.rhead) sits at the top of the visible
|
|
@@ -9452,6 +9534,11 @@ async function saEnhanceShotBoxes(){
|
|
|
9452
9534
|
}
|
|
9453
9535
|
function saRender(){
|
|
9454
9536
|
const S = $('seeall'); if (!S) return;
|
|
9537
|
+
// SSE, diff fetches and action responses rebuild the dialog. Remember the
|
|
9538
|
+
// review body's position before replacing it so reading never jumps to top.
|
|
9539
|
+
const previousBody = S.querySelector('.sa-body');
|
|
9540
|
+
const previousScrollTop = previousBody?.scrollTop ?? 0;
|
|
9541
|
+
const restoreScroll = Boolean(previousBody) && SA.focusGid == null;
|
|
9455
9542
|
const pending = SA.items.filter((x) => x.st === 0);
|
|
9456
9543
|
const green = pending.filter((x) => !x.testResult || x.testResult.ok !== false).length;
|
|
9457
9544
|
// Solo mode = a single "Needs you" goal opened as this card. It is not a review batch, so the
|
|
@@ -9464,6 +9551,16 @@ function saRender(){
|
|
|
9464
9551
|
const hdKeys = solo ? '↑↓ MOVE · ESC CLOSE' : '↑↓ MOVE · A APPROVE · D DISMISS · ESC CLOSE';
|
|
9465
9552
|
S.innerHTML = `<div class="sa-panel"><div class="sa-hd"><span class="t">${hdTitle}</span><span class="c">${c}</span><span class="sa-count">${posLabel}</span><span class="keys">${hdKeys}</span><button class="sa-x" data-saact="close:0">✕</button></div>
|
|
9466
9553
|
<div class="sa-body">${SA.items.length ? SA.items.map((it, i) => saItemHtml(it, i)).join('') : '<div class="sa-empty">Nothing waiting for review.</div>'}</div></div>`;
|
|
9554
|
+
const body = S.querySelector('.sa-body');
|
|
9555
|
+
if (body && restoreScroll) body.scrollTop = previousScrollTop;
|
|
9556
|
+
// Once the person scrolls, their position wins over the short opening-focus
|
|
9557
|
+
// animation. Programmatic opening scroll fires this too, which is fine: the
|
|
9558
|
+
// first placement happened and later SSE ticks should preserve it.
|
|
9559
|
+
body?.addEventListener('scroll', () => {
|
|
9560
|
+
if (SA.focusGid == null) return;
|
|
9561
|
+
SA.focusGid = null;
|
|
9562
|
+
if (SA.focusTimer) { clearTimeout(SA.focusTimer); SA.focusTimer = null; }
|
|
9563
|
+
}, { once: true });
|
|
9467
9564
|
// Native <details> would collapse on every rebuild (SSE tick, approve, etc.) —
|
|
9468
9565
|
// track what's open per goal+section in SA.detOpen (persists across re-renders)
|
|
9469
9566
|
// and wire each one's toggle to keep that set current.
|
|
@@ -9613,6 +9710,24 @@ async function saReplyCall(it, text, outcome){
|
|
|
9613
9710
|
if (!r?.ok) throw new Error('reply failed');
|
|
9614
9711
|
return r.json().catch(() => null);
|
|
9615
9712
|
}
|
|
9713
|
+
// Once feedback becomes rework, remove it from Review immediately and repaint the board
|
|
9714
|
+
// from the response. A queued reply appears in To Do; the next worker event promotes it to
|
|
9715
|
+
// Doing. Questions/unsure replies keep status=review and never come through this function.
|
|
9716
|
+
function saMoveFeedbackOutOfReview(it, body){
|
|
9717
|
+
const goal = body?.goal;
|
|
9718
|
+
const task = body?.task;
|
|
9719
|
+
if (goal?.id) upsert(state.goals, goal);
|
|
9720
|
+
if (task?.id) upsert(state.tasks, task);
|
|
9721
|
+
const at = SA.items.findIndex((row) => row.goalId === it.goalId);
|
|
9722
|
+
const next = at >= 0 ? (SA.items[at + 1] || SA.items[at - 1]) : null;
|
|
9723
|
+
if (at >= 0) SA.items.splice(at, 1);
|
|
9724
|
+
render();
|
|
9725
|
+
if (SA.solo === it.goalId || !SA.items.length) { saRender(); saClose(); return; }
|
|
9726
|
+
SA.cur = next?.goalId ?? SA.items[0]?.goalId ?? null;
|
|
9727
|
+
SA.focusGid = SA.cur;
|
|
9728
|
+
SA.focusScrolled = false;
|
|
9729
|
+
saRender();
|
|
9730
|
+
}
|
|
9616
9731
|
function saCardEl(goalId){ return document.querySelector(`#seeall .sa-item[data-gid="${goalId}"]`); }
|
|
9617
9732
|
// Flip one card to its "Working on it" head without redrawing anything else (決定2).
|
|
9618
9733
|
function saFlipWorking(goalId){
|
|
@@ -9641,11 +9756,18 @@ function saPaintCard(goalId){
|
|
|
9641
9756
|
// instruction (continue).
|
|
9642
9757
|
async function saSendReply(it, i, text){
|
|
9643
9758
|
const goalId = it.goalId;
|
|
9759
|
+
const attached = (SA.attachments[goalId] || []).slice();
|
|
9760
|
+
const displayText = text || (attached.length === 1 ? 'Attached an image.' : `Attached ${attached.length} images.`);
|
|
9761
|
+
const workerText = attached.length
|
|
9762
|
+
? `${text}${text ? '\n\n' : ''}Attached images (read these absolute paths): ${attached.map((a) => a.path).join(', ')}`
|
|
9763
|
+
: text;
|
|
9764
|
+
text = displayText;
|
|
9644
9765
|
await saEnsureThread(it); // fold in any existing history BEFORE this reply (so it isn't hidden)
|
|
9645
9766
|
const seq = saNextSeq(); // reserve send order NOW, so a late-resolving route keeps its place
|
|
9646
9767
|
// Clear the box in place — keep it FOCUSED, not blurred (saSync skips a rebuild while an
|
|
9647
9768
|
// input in #seeall has focus, which is what keeps an SSE tick from flashing the panel).
|
|
9648
9769
|
document.querySelectorAll(`#seeall .sa-item[data-gid="${goalId}"] .sa-chat`).forEach((inp) => { inp.value = ''; });
|
|
9770
|
+
delete SA.attachments[goalId]; saPaintAttachments(goalId);
|
|
9649
9771
|
// Append the line to the card's own thread immediately — this is what the person sees, and
|
|
9650
9772
|
// its `seq` fixes its order regardless of when any round trip returns.
|
|
9651
9773
|
it.rthread = [...(it.rthread || []), { who: 'you', text, seq }];
|
|
@@ -9658,7 +9780,7 @@ async function saSendReply(it, i, text){
|
|
|
9658
9780
|
it.st = 3; it.rstatus = 'working'; SA.cur = goalId;
|
|
9659
9781
|
saPaintCard(goalId);
|
|
9660
9782
|
try {
|
|
9661
|
-
const r = await fetch(withKey(`/api/goals/${goalId}/reply`), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ text }) });
|
|
9783
|
+
const r = await fetch(withKey(`/api/goals/${goalId}/reply`), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ text: workerText }) });
|
|
9662
9784
|
if (!r.ok) throw new Error('reply failed');
|
|
9663
9785
|
if (isAttn) {
|
|
9664
9786
|
// A Needs-you reply picks the goal back up → it belongs in Doing, not lingering in
|
|
@@ -9669,6 +9791,7 @@ async function saSendReply(it, i, text){
|
|
|
9669
9791
|
render(); // saSync skips the focused card, so this only refreshes the lane behind it
|
|
9670
9792
|
}
|
|
9671
9793
|
} catch {
|
|
9794
|
+
SA.attachments[goalId] = attached; saPaintAttachments(goalId);
|
|
9672
9795
|
it.rthread = it.rthread.filter((m) => m.seq !== seq); // roll back just this line
|
|
9673
9796
|
if (!it.rthread.some((m) => m.who === 'you')) { it.st = 0; it.rstatus = null; }
|
|
9674
9797
|
saPaintCard(goalId); showErr('Reply failed.');
|
|
@@ -9681,7 +9804,7 @@ async function saSendReply(it, i, text){
|
|
|
9681
9804
|
it.inflight = true; it.rstatus = 'thinking'; SA.cur = goalId;
|
|
9682
9805
|
saPaintCard(goalId);
|
|
9683
9806
|
try {
|
|
9684
|
-
let body = await saReplyCall(it,
|
|
9807
|
+
let body = await saReplyCall(it, workerText, 'auto');
|
|
9685
9808
|
if (body?.outcome === 'unsure') {
|
|
9686
9809
|
it.inflight = false; it.rstatus = 'unsure'; it.rchoices = body.choices || REPLY_OUTCOMES_CLIENT;
|
|
9687
9810
|
saPaintCard(goalId);
|
|
@@ -9693,7 +9816,7 @@ async function saSendReply(it, i, text){
|
|
|
9693
9816
|
saPaintCard(goalId); return;
|
|
9694
9817
|
}
|
|
9695
9818
|
it.inflight = true; it.rstatus = 'thinking'; saPaintCard(goalId);
|
|
9696
|
-
body = await saReplyCall(it,
|
|
9819
|
+
body = await saReplyCall(it, workerText, picked);
|
|
9697
9820
|
}
|
|
9698
9821
|
it.inflight = false;
|
|
9699
9822
|
if (body?.outcome === 'answer') { // a question: the AI answers, the goal never moves
|
|
@@ -9703,10 +9826,17 @@ async function saSendReply(it, i, text){
|
|
|
9703
9826
|
it.rstatus = null; saPaintCard(goalId);
|
|
9704
9827
|
return;
|
|
9705
9828
|
}
|
|
9706
|
-
//
|
|
9707
|
-
|
|
9829
|
+
// A change request has left Review. The response is the source of truth for whether it
|
|
9830
|
+
// is queued (To Do) or already starting (Doing); reveal that transition immediately.
|
|
9831
|
+
if (body?.goal && body.goal.status !== 'review') {
|
|
9832
|
+
saMoveFeedbackOutOfReview(it, body);
|
|
9833
|
+
return;
|
|
9834
|
+
}
|
|
9835
|
+
// split keeps the original goal in Review; only the newly-created ticket moves away.
|
|
9836
|
+
it.rstatus = null;
|
|
9708
9837
|
saPaintCard(goalId);
|
|
9709
9838
|
} catch {
|
|
9839
|
+
SA.attachments[goalId] = attached; saPaintAttachments(goalId);
|
|
9710
9840
|
it.inflight = false;
|
|
9711
9841
|
it.rthread = it.rthread.filter((m) => m.seq !== seq);
|
|
9712
9842
|
if (!it.rthread.length) it.rstatus = null;
|
|
@@ -9736,6 +9866,27 @@ async function saAct(it, st, path, body){
|
|
|
9736
9866
|
const SA_UNDO = { 1: 'revert-review', 2: 'reopen', 3: 'undismiss', 4: 'unarchive' }; // 07-20c: st 2 = Reject, undo = reopen
|
|
9737
9867
|
$('seeall').addEventListener('mousedown', (e) => { if (e.target === $('seeall')) saClose(); }); // no-dim dialog: outside = close
|
|
9738
9868
|
$('seeall').addEventListener('click', async (e) => {
|
|
9869
|
+
// Toggle only the diff in place. Avoiding a full render preserves the review scroll position.
|
|
9870
|
+
const diffMore = e.target.closest('[data-ghmore]');
|
|
9871
|
+
if (diffMore) {
|
|
9872
|
+
const goalId = Number(diffMore.dataset.ghmore);
|
|
9873
|
+
const open = !SA.diffOpen.has(goalId);
|
|
9874
|
+
if (open) SA.diffOpen.add(goalId); else SA.diffOpen.delete(goalId);
|
|
9875
|
+
diffMore.previousElementSibling?.classList.toggle('clip', !open);
|
|
9876
|
+
diffMore.classList.toggle('open', open);
|
|
9877
|
+
diffMore.setAttribute('aria-expanded', String(open));
|
|
9878
|
+
const label = diffMore.querySelector('.ghlabel');
|
|
9879
|
+
if (label) label.innerHTML = open ? 'Collapse' : `Show all <span class="n">${diffMore.dataset.total}</span> lines`;
|
|
9880
|
+
return;
|
|
9881
|
+
}
|
|
9882
|
+
const removeImage = e.target.closest('[data-saarm]');
|
|
9883
|
+
if (removeImage) {
|
|
9884
|
+
const [goalId, index] = removeImage.dataset.saarm.split(':').map(Number);
|
|
9885
|
+
SA.attachments[goalId]?.splice(index, 1);
|
|
9886
|
+
if (!SA.attachments[goalId]?.length) delete SA.attachments[goalId];
|
|
9887
|
+
saPaintAttachments(goalId);
|
|
9888
|
+
return;
|
|
9889
|
+
}
|
|
9739
9890
|
// "+N more" in a conversation: show the whole thing, in place. Nothing was summarised
|
|
9740
9891
|
// away, so this only has to stop hiding it (§B3).
|
|
9741
9892
|
const more = e.target.closest('.convmore');
|
|
@@ -9779,7 +9930,7 @@ $('seeall').addEventListener('click', async (e) => {
|
|
|
9779
9930
|
if (link) link.disabled = true;
|
|
9780
9931
|
if (v) v.textContent = 'Starting it…';
|
|
9781
9932
|
let r = null, body = {};
|
|
9782
|
-
try { r = await fetch(`/api/tasks/${it.run.taskId}/preview
|
|
9933
|
+
try { r = await fetch(withKey(`/api/tasks/${it.run.taskId}/preview`), { method: 'POST' }); body = await r.json().catch(() => ({})); }
|
|
9783
9934
|
catch (e) { body = { error: String(e.message ?? e) }; }
|
|
9784
9935
|
if (link) link.disabled = false;
|
|
9785
9936
|
const noteText = it.run.note || it.run.cmd || 'Starts when you press it.';
|
|
@@ -9808,7 +9959,7 @@ $('seeall').addEventListener('click', async (e) => {
|
|
|
9808
9959
|
// same round-trip as Enter-in-.sa-chat below — one send-back code path either way.
|
|
9809
9960
|
const inp = $('seeall').querySelector(`.sa-chat[data-i="${i}"]`);
|
|
9810
9961
|
const text = inp?.value.trim();
|
|
9811
|
-
if (!text) { inp?.focus(); return; }
|
|
9962
|
+
if (!text && !SA.attachments[it.goalId]?.length) { inp?.focus(); return; }
|
|
9812
9963
|
return saSendReply(it, i, text);
|
|
9813
9964
|
}
|
|
9814
9965
|
if (k === 'undo') {
|
|
@@ -9824,11 +9975,20 @@ $('seeall').addEventListener('click', async (e) => {
|
|
|
9824
9975
|
$('seeall').addEventListener('keydown', (e) => {
|
|
9825
9976
|
if (!e.target.classList?.contains('sa-chat') || e.key !== 'Enter' || e.isComposing) return;
|
|
9826
9977
|
const text = e.target.value.trim();
|
|
9827
|
-
if (!text) return;
|
|
9828
|
-
e.preventDefault();
|
|
9829
9978
|
const it = SA.items[Number(e.target.dataset.i)];
|
|
9979
|
+
if (!text && !SA.attachments[it?.goalId]?.length) return;
|
|
9980
|
+
e.preventDefault();
|
|
9830
9981
|
if (it) saSendReply(it, Number(e.target.dataset.i), text); // no extra confirm (PRD §5.1): Enter = sent
|
|
9831
9982
|
});
|
|
9983
|
+
$('seeall').addEventListener('paste', (e) => {
|
|
9984
|
+
const input = e.target.closest('.sa-chat');
|
|
9985
|
+
if (!input) return;
|
|
9986
|
+
const images = [...(e.clipboardData?.items || [])].filter((item) => item.type.startsWith('image/'));
|
|
9987
|
+
if (!images.length) return;
|
|
9988
|
+
e.preventDefault();
|
|
9989
|
+
const goalId = Number(input.dataset.goal);
|
|
9990
|
+
for (const item of images) saAttachImage(goalId, item.getAsFile());
|
|
9991
|
+
});
|
|
9832
9992
|
document.addEventListener('keydown', (e) => {
|
|
9833
9993
|
if (!SA.open || ['INPUT', 'TEXTAREA'].includes(e.target.tagName)) return;
|
|
9834
9994
|
if (e.key === 'Escape') return saClose();
|
package/engine/lib.mjs
CHANGED
|
@@ -1646,6 +1646,25 @@ export function detectRequestLanguage(text) {
|
|
|
1646
1646
|
return /[-ヿ㐀-鿿]/.test(text || '') ? 'ja' : 'en';
|
|
1647
1647
|
}
|
|
1648
1648
|
|
|
1649
|
+
export function testFailureReason({ text = '', count = 0, attempts = null } = {}) {
|
|
1650
|
+
const n = Number(count) || 0;
|
|
1651
|
+
const tries = attempts == null ? null : Number(attempts) || 0;
|
|
1652
|
+
if (detectRequestLanguage(text) === 'ja') {
|
|
1653
|
+
return tries == null ? `テスト失敗 ${n} 件` : `テスト失敗 ${n} 件(自動修正 ${tries} 回後も失敗)`;
|
|
1654
|
+
}
|
|
1655
|
+
const tests = `${n} test${n === 1 ? '' : 's'} failed`;
|
|
1656
|
+
return tries == null ? tests : `${tests} after ${tries} automatic fix attempt${tries === 1 ? '' : 's'}`;
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
export function manualTestRetryPrompt({ text = '', count = 0, detail = '' } = {}) {
|
|
1660
|
+
const n = Number(count) || 0;
|
|
1661
|
+
const output = String(detail || '(no detailed output captured)').slice(0, 3200);
|
|
1662
|
+
if (detectRequestLanguage(text) === 'ja') {
|
|
1663
|
+
return `Retryが押されました。前回のテスト失敗 ${n} 件を確認し、元のタスクの作業環境で原因を修正してからテストを再実行してください。関係のないリファクタは行わないでください。\n\n前回のテスト出力:\n${output}`;
|
|
1664
|
+
}
|
|
1665
|
+
return `Retry was requested. Inspect the previous ${n} failing test${n === 1 ? '' : 's'}, fix the cause in the original task worktree, then rerun the tests. Do not make unrelated changes.\n\nPrevious test output:\n${output}`;
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1649
1668
|
// SLICE 2 intent routing (docs/HANDOFF-ONBOARDING-conversational.md): the single
|
|
1650
1669
|
// composer is conversation-first — a chat/help/settings message ("what can you
|
|
1651
1670
|
// do?", "reply in English", a greeting) must get a conversational reply, NOT a
|
package/engine/server.mjs
CHANGED
|
@@ -21,12 +21,12 @@ 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, isForcedStop, taskStatusAfterVerify, workerResultText, taskCountsAsComplete, resolveWantsPR, resolveGoalSource, buildGoalSummary, 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, hasTestRelevantChanges, parseFailingTestNames, classifyTestGate, isInconclusiveTestRun, countInfraFlakes, classifyRunFailures, classifyVerifyGate, 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, isNothingVerifiable, classifyComposerIntentHeuristic, detectPauseIntent, buildIntentPrompt, parseIntentResponse, buildBoardSnapshot, validateBoardSnapshot, boardIsEmpty, decideBoardPull, resolveConnectedAgents, pickAvailableAgent, pickUtilityAgent, utilityModel, liveTakeoverDecision, shouldOpenBrowserOnBoot, goalTaskTitle, buildGoalTask, RUN_DECL_FILE, parseRunDeclaration, SHOT_DIR, collectShotNames, parseGoalAddress, REPLY_OUTCOMES, REPLY_INTENTS, buildReplyIntentPrompt, parseReplyIntent, buildGoalMessage, serializeGoalMessage, parseGoalMessages } from './lib.mjs';
|
|
24
|
+
import { parseStreamEvents, parseCodexEvents, buildCodexArgs, parsePlan, refinePlanTasks, canEditGoal, canDeleteGoal, shouldAskClarification, workerExitReason, isForcedStop, taskStatusAfterVerify, workerResultText, taskCountsAsComplete, resolveWantsPR, resolveGoalSource, buildGoalSummary, 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, hasTestRelevantChanges, parseFailingTestNames, classifyTestGate, isInconclusiveTestRun, countInfraFlakes, classifyRunFailures, classifyVerifyGate, 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, RUN_DECL_FILE, parseRunDeclaration, SHOT_DIR, collectShotNames, parseGoalAddress, REPLY_OUTCOMES, REPLY_INTENTS, buildReplyIntentPrompt, parseReplyIntent, buildGoalMessage, serializeGoalMessage, parseGoalMessages } from './lib.mjs';
|
|
25
25
|
import { createSerialQueue } from './lib.mjs';
|
|
26
26
|
import { collectProjectRules } from './lib.mjs';
|
|
27
27
|
import { FALLBACK_CODEX_MODELS, loadCodexModels } from './codex-models.mjs';
|
|
28
28
|
import { CLAUDE_MODELS } from './claude-models.mjs';
|
|
29
|
-
import { classifyWorkerPreflight, workerCanStart } from './worker-availability.mjs';
|
|
29
|
+
import { classifyWorkerPreflight, shouldProbeClaudeAuth, workerCanStart } from './worker-availability.mjs';
|
|
30
30
|
import { openPR } from './pr.mjs';
|
|
31
31
|
import { runVerification, exerciseUi } from './verify.mjs';
|
|
32
32
|
|
|
@@ -1183,6 +1183,7 @@ function runClaude({ prompt, cwd, tools, onEvent, onTodos, resume, model, effort
|
|
|
1183
1183
|
}
|
|
1184
1184
|
});
|
|
1185
1185
|
child.stderr.on('data', (d) => { err += d; });
|
|
1186
|
+
child.on('error', (e) => { err += `${e.message}\n`; });
|
|
1186
1187
|
child.on('close', (code) => {
|
|
1187
1188
|
clearTimeout(timer);
|
|
1188
1189
|
done({ code, timedOut, result: workerResultText({ result, err, code, timedOut, lastAct, timeoutMs: WORKER_TIMEOUT_MS }), sessionId, usage });
|
|
@@ -1229,6 +1230,7 @@ function runCodex({ prompt, cwd, onEvent, onTodos, resume, permissionMode = 'acc
|
|
|
1229
1230
|
}
|
|
1230
1231
|
});
|
|
1231
1232
|
child.stderr.on('data', (d) => { err += d; });
|
|
1233
|
+
child.on('error', (e) => { err += `${e.message}\n`; });
|
|
1232
1234
|
child.on('close', (code) => {
|
|
1233
1235
|
clearTimeout(timer);
|
|
1234
1236
|
done({ code, timedOut, result: workerResultText({ result, err, code, timedOut, lastAct, timeoutMs: WORKER_TIMEOUT_MS }), sessionId, usage });
|
|
@@ -1485,13 +1487,16 @@ function startNextStacked(projectId) {
|
|
|
1485
1487
|
// in". We probe once at boot and hold the queue + raise a banner instead of
|
|
1486
1488
|
// failing task after task. Claude waits for the short probe; Codex is gated
|
|
1487
1489
|
// independently and may start immediately.
|
|
1488
|
-
let workerAuth =
|
|
1490
|
+
let workerAuth = shouldProbeClaudeAuth(AVAILABLE_AGENTS)
|
|
1491
|
+
? { ok: null, reason: null, checkedAt: null }
|
|
1492
|
+
: { ok: true, reason: null, checkedAt: null };
|
|
1489
1493
|
const publicAuth = () => ({ ok: workerAuth.ok, reason: workerAuth.reason });
|
|
1490
1494
|
|
|
1491
1495
|
// Boot preflight: one cheap Read-only `claude` call; classify the result (pure,
|
|
1492
1496
|
// in lib) and broadcast so the UI can show/clear the banner. `claude -p` exits 0
|
|
1493
1497
|
// even when logged out, so classifyAuthProbe reads the text, not the code.
|
|
1494
1498
|
async function probeWorkerAuth() {
|
|
1499
|
+
if (!shouldProbeClaudeAuth(AVAILABLE_AGENTS)) return workerAuth;
|
|
1495
1500
|
try {
|
|
1496
1501
|
const out = await runClaude({ prompt: 'Reply with the single word: ok', cwd: ROOT, tools: 'Read', model: 'haiku' });
|
|
1497
1502
|
workerAuth = {
|
|
@@ -1509,6 +1514,20 @@ async function probeWorkerAuth() {
|
|
|
1509
1514
|
return workerAuth;
|
|
1510
1515
|
}
|
|
1511
1516
|
|
|
1517
|
+
function openClaudeLogin() {
|
|
1518
|
+
const command = 'claude auth login';
|
|
1519
|
+
if (process.platform === 'darwin') {
|
|
1520
|
+
execFile('osascript', ['-e', `tell application "Terminal" to do script "${command}"`, '-e', 'tell application "Terminal" to activate'], () => {});
|
|
1521
|
+
return true;
|
|
1522
|
+
}
|
|
1523
|
+
if (process.platform === 'win32') {
|
|
1524
|
+
const child = spawn('cmd.exe', ['/c', 'start', '', 'cmd.exe', '/k', command], { detached: true, stdio: 'ignore' });
|
|
1525
|
+
child.once('error', () => {}); child.unref(); return true;
|
|
1526
|
+
}
|
|
1527
|
+
const child = spawn('x-terminal-emulator', ['-e', command], { detached: true, stdio: 'ignore' });
|
|
1528
|
+
child.once('error', () => {}); child.unref(); return true;
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1512
1531
|
// Layer 2 of the intent router: a general planner clarify card has no builder to
|
|
1513
1532
|
// re-render from, so when the user asks to switch language mid-question we ask
|
|
1514
1533
|
// the model (cheap, Read-only, no repo edits) to RESTATE the pending question in
|
|
@@ -3187,7 +3206,7 @@ async function verifyGate(goal, prProject, siblings, baseProject, activityTask =
|
|
|
3187
3206
|
if (testsFailing) {
|
|
3188
3207
|
const failure = recordGoalFailure(goal, {
|
|
3189
3208
|
kind: 'test',
|
|
3190
|
-
reason:
|
|
3209
|
+
reason: testFailureReason({ text: goal.text, count: blockingFailCount }),
|
|
3191
3210
|
changedFiles,
|
|
3192
3211
|
testResult: goal.testResult,
|
|
3193
3212
|
});
|
|
@@ -3199,7 +3218,7 @@ async function verifyGate(goal, prProject, siblings, baseProject, activityTask =
|
|
|
3199
3218
|
goal.autoRework = {
|
|
3200
3219
|
...(goal.autoRework ?? {}),
|
|
3201
3220
|
testFailures: used + 1,
|
|
3202
|
-
lastReason:
|
|
3221
|
+
lastReason: testFailureReason({ text: goal.text, count: blockingFailCount }),
|
|
3203
3222
|
lastAt: new Date().toISOString(),
|
|
3204
3223
|
};
|
|
3205
3224
|
goal.blocked = null;
|
|
@@ -3245,7 +3264,7 @@ async function verifyGate(goal, prProject, siblings, baseProject, activityTask =
|
|
|
3245
3264
|
}
|
|
3246
3265
|
goal.blocked = {
|
|
3247
3266
|
kind: gate.kind,
|
|
3248
|
-
reason: testsFailing ?
|
|
3267
|
+
reason: testsFailing ? testFailureReason({ text: goal.text, count: blockingFailCount, attempts: goal.autoRework?.testFailures ?? 0 })
|
|
3249
3268
|
: nothingVerifiable ? '検証条件つきのタスクなのに、ファイル変更も proof も無い(約束した検証が行われていない)'
|
|
3250
3269
|
: '検証(proof)が全タスク分そろっていない(requireVerifyPass)',
|
|
3251
3270
|
};
|
|
@@ -3588,6 +3607,33 @@ const server = createServer(async (req, res) => {
|
|
|
3588
3607
|
return json(res, r.action === 'busy' ? 409 : 200, r);
|
|
3589
3608
|
}
|
|
3590
3609
|
|
|
3610
|
+
// Human-sized recovery actions for the worker banner. Login opens Claude's
|
|
3611
|
+
// official interactive auth flow; rate-limit recovery moves only this
|
|
3612
|
+
// identity's queued Claude work and only after an explicit button press.
|
|
3613
|
+
if (url.pathname === '/api/worker-auth/connect' && req.method === 'POST') {
|
|
3614
|
+
return json(res, openClaudeLogin() ? 200 : 500, { opened: true });
|
|
3615
|
+
}
|
|
3616
|
+
if (url.pathname === '/api/worker-auth/retry' && req.method === 'POST') {
|
|
3617
|
+
const auth = await probeWorkerAuth();
|
|
3618
|
+
if (auth.ok) for (const project of projects) pump(project.id);
|
|
3619
|
+
return json(res, 200, { auth: publicAuth() });
|
|
3620
|
+
}
|
|
3621
|
+
if (url.pathname === '/api/worker-auth/run-with-codex' && req.method === 'POST') {
|
|
3622
|
+
if (workerAuth.reason !== 'rate-limited') return json(res, 409, { error: 'Claude work is not rate limited' });
|
|
3623
|
+
if (!AVAILABLE_AGENTS.includes('codex')) return json(res, 409, { error: 'Codex is not connected on this device' });
|
|
3624
|
+
const identity = identityFor(req);
|
|
3625
|
+
const visible = new Set(goals.filter((goal) => goalVisibleTo(goal, identity, MANAGER_OWNER_EMAIL)).map((goal) => goal.id));
|
|
3626
|
+
const queued = tasks.filter((task) => visible.has(task.goalId) && task.status === 'queued' && task.agent === 'claude-code');
|
|
3627
|
+
const goalIds = new Set(queued.map((task) => task.goalId));
|
|
3628
|
+
const model = CODEX_MODELS[0];
|
|
3629
|
+
for (const task of queued) { task.agent = 'codex'; task.model = model; task.sessionId = null; saveTask(task); }
|
|
3630
|
+
for (const goal of goals.filter((item) => goalIds.has(item.id))) {
|
|
3631
|
+
goal.agent = 'codex'; goal.model = model; goal.sessionId = null; goal.startedAt = null; saveGoal(goal);
|
|
3632
|
+
}
|
|
3633
|
+
for (const projectId of new Set(queued.map((task) => task.projectId))) pump(projectId);
|
|
3634
|
+
return json(res, 200, { changed: queued.length, goals: [...goalIds] });
|
|
3635
|
+
}
|
|
3636
|
+
|
|
3591
3637
|
// The folders a person could actually point a project at: the git repos sitting
|
|
3592
3638
|
// directly under HOME. A browser cannot open an OS folder dialog that yields a real
|
|
3593
3639
|
// path (showDirectoryPicker hands back a handle, never a path), so a detected list
|
|
@@ -4538,8 +4584,8 @@ const server = createServer(async (req, res) => {
|
|
|
4538
4584
|
return json(res, 200, goal);
|
|
4539
4585
|
}
|
|
4540
4586
|
|
|
4541
|
-
//
|
|
4542
|
-
//
|
|
4587
|
+
// Retry a test-blocked goal by sending the saved failure back to its worker.
|
|
4588
|
+
// Other blocked states still re-run the verification gate in place.
|
|
4543
4589
|
const reverifyMatch = url.pathname.match(/^\/api\/goals\/(\d+)\/reverify$/);
|
|
4544
4590
|
if (reverifyMatch && req.method === 'POST') {
|
|
4545
4591
|
const goal = goals.find((g) => g.id === Number(reverifyMatch[1]));
|
|
@@ -4548,6 +4594,19 @@ const server = createServer(async (req, res) => {
|
|
|
4548
4594
|
const project = projects.find((p) => p.id === goal.projectId);
|
|
4549
4595
|
if (!project) return json(res, 404, { error: 'project not found' });
|
|
4550
4596
|
const siblings = tasks.filter((t) => t.goalId === goal.id);
|
|
4597
|
+
if (goal.status === 'blocked' && goal.blocked?.kind === 'test') {
|
|
4598
|
+
const count = goal.testResult?.newFailures?.length ?? goal.testResult?.failed ?? 0;
|
|
4599
|
+
const prompt = manualTestRetryPrompt({ text: goal.text, count, detail: goal.testResult?.detail });
|
|
4600
|
+
goal.status = 'running';
|
|
4601
|
+
goal.blocked = null;
|
|
4602
|
+
goal.startedAt = null;
|
|
4603
|
+
// A manual retry is a fresh user decision, independent of the automatic
|
|
4604
|
+
// two-attempt budget. Keep the selected agent/model and same conversation.
|
|
4605
|
+
goal.autoRework = { ...(goal.autoRework ?? {}), testFailures: 0, manualRetryAt: new Date().toISOString() };
|
|
4606
|
+
saveGoal(goal);
|
|
4607
|
+
const task = queueReplyTask(goal, prompt, { from: 'manager', via: 'retry' });
|
|
4608
|
+
return json(res, 200, { goal, task, repairing: true });
|
|
4609
|
+
}
|
|
4551
4610
|
const wdir = (goalWorkDirs.get(goal.id) && existsSync(goalWorkDirs.get(goal.id))) ? goalWorkDirs.get(goal.id) : project.dir;
|
|
4552
4611
|
// 再検証 is the explicit human "run the tests now" — force the gate on for this
|
|
4553
4612
|
// one goal even when the default (automatic) gate is off.
|
|
@@ -5003,9 +5062,9 @@ server.listen(PORT, '127.0.0.1', () => {
|
|
|
5003
5062
|
spawn('open', ['-g', openUrl], { stdio: 'ignore' }).unref();
|
|
5004
5063
|
}
|
|
5005
5064
|
console.log(`[manager] projects: ${projects.map((p) => `${p.id}=${p.dir}`).join(' ')}`);
|
|
5006
|
-
probeWorkerAuth().catch(() => {}); // hold Claude tasks + banner if Claude is unavailable
|
|
5065
|
+
if (shouldProbeClaudeAuth(AVAILABLE_AGENTS)) probeWorkerAuth().catch(() => {}); // hold Claude tasks + banner if Claude is unavailable
|
|
5007
5066
|
setInterval(() => {
|
|
5008
|
-
if (!workerAuth.ok) probeWorkerAuth().catch(() => {});
|
|
5067
|
+
if (shouldProbeClaudeAuth(AVAILABLE_AGENTS) && !workerAuth.ok) probeWorkerAuth().catch(() => {});
|
|
5009
5068
|
}, 5 * 60_000).unref();
|
|
5010
5069
|
syncAllExternal().catch(() => {});
|
|
5011
5070
|
syncGoalMerges().catch(() => {});
|
|
@@ -6,3 +6,7 @@ export function classifyWorkerPreflight({ result = '', code = 0 } = {}, { classi
|
|
|
6
6
|
export function workerCanStart({ agent, auth } = {}) {
|
|
7
7
|
return auth?.ok === true || agent === 'codex';
|
|
8
8
|
}
|
|
9
|
+
|
|
10
|
+
export function shouldProbeClaudeAuth(agents = []) {
|
|
11
|
+
return agents.includes('claude-code');
|
|
12
|
+
}
|
package/package.json
CHANGED