@galda/cli 0.10.43 → 0.10.45
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 +192 -36
- package/engine/lib.mjs +43 -0
- package/engine/server.mjs +31 -11
- package/package.json +1 -1
package/app/index.html
CHANGED
|
@@ -2046,6 +2046,14 @@
|
|
|
2046
2046
|
.redesign .rtag{letter-spacing:.02em;font-variant-numeric:tabular-nums}
|
|
2047
2047
|
.redesign .rst{display:inline-flex;align-items:center;gap:6px;color:var(--ink2)}
|
|
2048
2048
|
.redesign .rdot{width:6px;height:6px;border-radius:50%;background:var(--amber,#e0a021);display:inline-block}
|
|
2049
|
+
/* reply → in-place "Working on it" (REVIEW-FEEDBACK-FLOW 決定1-4): a reply does NOT
|
|
2050
|
+
dismiss the card. The header turns green "Working on it"; your line + the AI's one-line
|
|
2051
|
+
voice live in the reply thread below the box (.replynote .wkline) so sending only appends
|
|
2052
|
+
— no re-layout, no flash. */
|
|
2053
|
+
.redesign .rst.working{color:var(--green)}
|
|
2054
|
+
.redesign .rst.working .rdot{background:var(--green)}
|
|
2055
|
+
.sa-item.working{box-shadow:0 1px 2px rgba(20,23,31,.05),0 0 0 1px color-mix(in oklab,var(--green) 22%,var(--hair))}
|
|
2056
|
+
.replynote .wkline .wkdot{display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--green);box-shadow:0 0 0 3px color-mix(in oklab,var(--green) 22%,transparent);margin-right:9px;vertical-align:middle;position:relative;top:-1px}
|
|
2049
2057
|
.redesign .rtitle{font-family:var(--ui);font-size:18px;font-weight:680;letter-spacing:-.018em;line-height:1.3;margin:8px 0 0 !important;color:var(--ink)}
|
|
2050
2058
|
/* meta — Linear sans, green/neutral chips, hairline under (spec §4) */
|
|
2051
2059
|
.redesign .rmeta{display:flex;flex-wrap:wrap;align-items:center;gap:8px 12px;margin:12px 0 0 !important;padding-bottom:12px !important;
|
|
@@ -4748,7 +4756,7 @@ function renderTasks(){
|
|
|
4748
4756
|
}
|
|
4749
4757
|
for (const inp of $('tasklist').querySelectorAll('[data-qreplyinput]')) {
|
|
4750
4758
|
inp.onclick = (e) => e.stopPropagation();
|
|
4751
|
-
inp.onkeydown = (e) => { if (e.key === 'Enter') { e.preventDefault(); sendQReply(inp.dataset.qreplyinput, inp); } };
|
|
4759
|
+
inp.onkeydown = (e) => { if (e.key === 'Enter' && !e.isComposing) { e.preventDefault(); sendQReply(inp.dataset.qreplyinput, inp); } };
|
|
4752
4760
|
}
|
|
4753
4761
|
// Pending rows open the same detail dialog as Review (tap anywhere but the
|
|
4754
4762
|
// Start/Delete buttons, which stop propagation in their own handlers).
|
|
@@ -4872,7 +4880,7 @@ function startGoalheadEdit(id, btn){
|
|
|
4872
4880
|
const save = async () => { const t = inp.value.trim(); if (!t) { wrap.remove(); return; }
|
|
4873
4881
|
const r = await fetch(withKey(`/api/goals/${id}`), { method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ text: t }) });
|
|
4874
4882
|
if (!r.ok) showErr('Cannot edit a running/finished goal — reply to append instead'); await refresh(); };
|
|
4875
|
-
inp.onkeydown = (e) => { if (e.key === 'Enter') { e.preventDefault(); save(); } if (e.key === 'Escape') wrap.remove(); };
|
|
4883
|
+
inp.onkeydown = (e) => { if (e.key === 'Enter' && !e.isComposing) { e.preventDefault(); save(); } if (e.key === 'Escape') wrap.remove(); };
|
|
4876
4884
|
}
|
|
4877
4885
|
// SL1 inline Reply: append an instruction to a started goal → POST /api/goals/:id/reply.
|
|
4878
4886
|
function openGoalheadReply(id, btn){
|
|
@@ -4885,7 +4893,7 @@ function openGoalheadReply(id, btn){
|
|
|
4885
4893
|
const send = async () => { const t = inp.value.trim(); if (!t) return;
|
|
4886
4894
|
const r = await fetch(withKey(`/api/goals/${id}/reply`), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ text: t }) });
|
|
4887
4895
|
if (!r.ok) showErr('未開始のゴールは編集を使ってください'); await refresh(); };
|
|
4888
|
-
inp.onkeydown = (e) => { if (e.key === 'Enter') { e.preventDefault(); send(); } if (e.key === 'Escape') wrap.remove(); };
|
|
4896
|
+
inp.onkeydown = (e) => { if (e.key === 'Enter' && !e.isComposing) { e.preventDefault(); send(); } if (e.key === 'Escape') wrap.remove(); };
|
|
4889
4897
|
snd.onclick = send;
|
|
4890
4898
|
}
|
|
4891
4899
|
|
|
@@ -5620,9 +5628,19 @@ function paintReplyNote(){
|
|
|
5620
5628
|
function paintSettledThreads(){
|
|
5621
5629
|
for (const inp of document.querySelectorAll('#seeall .sa-chat[data-goal]')) {
|
|
5622
5630
|
const goalId = Number(inp.dataset.goal);
|
|
5623
|
-
loadReplyThread(goalId);
|
|
5624
5631
|
if (replyNoteState?.goalId === goalId) continue; // the live note draws its own history
|
|
5625
|
-
const
|
|
5632
|
+
const item = typeof SA !== 'undefined' ? SA.items.find((x) => x.goalId === goalId) : null;
|
|
5633
|
+
let html;
|
|
5634
|
+
// A card with a live See-all conversation (it.rthread / a transient status) renders from
|
|
5635
|
+
// its OWN client state — send-ordered, per-card, never re-fetched. This is what keeps
|
|
5636
|
+
// replies from vanishing, reordering, or wiping another card (Masa 2026-07-24). A card
|
|
5637
|
+
// with no client thread (reopened later) falls back to the server-persisted thread.
|
|
5638
|
+
if (item && (item.rthread?.length || item.rstatus)) {
|
|
5639
|
+
html = saCardThreadHtml(item);
|
|
5640
|
+
} else {
|
|
5641
|
+
loadReplyThread(goalId);
|
|
5642
|
+
html = replyPastHtmlFor(goalId);
|
|
5643
|
+
}
|
|
5626
5644
|
if (!html) continue;
|
|
5627
5645
|
const row = inp.closest('.sa-act') ?? inp.parentElement;
|
|
5628
5646
|
if (!row?.parentElement) continue;
|
|
@@ -5630,6 +5648,8 @@ function paintSettledThreads(){
|
|
|
5630
5648
|
el.className = 'qsub replynote';
|
|
5631
5649
|
el.innerHTML = html;
|
|
5632
5650
|
row.after(el);
|
|
5651
|
+
// wire the unsure route chips (they carry the goal id so a repaint keeps them working)
|
|
5652
|
+
for (const b of el.querySelectorAll('[data-rcgoal]')) b.onclick = () => saPickRoute(Number(b.dataset.rcgoal), b.dataset.rc);
|
|
5633
5653
|
}
|
|
5634
5654
|
}
|
|
5635
5655
|
function setReplyNote(anchorSel, html, resolve){
|
|
@@ -5661,6 +5681,9 @@ function replyPastHtml(){
|
|
|
5661
5681
|
return replyPastHtmlFor(goalId);
|
|
5662
5682
|
}
|
|
5663
5683
|
function replyPastHtmlFor(goalId){
|
|
5684
|
+
// The SERVER-persisted thread (via:'review'). Used for a card reopened later (its client
|
|
5685
|
+
// it.rthread is gone) and for the #reviewFixInput dialog. The live See-all card renders from
|
|
5686
|
+
// its own client it.rthread (saCardThreadHtml) instead — see paintSettledThreads.
|
|
5664
5687
|
const msgs = goalId == null ? [] : (replyThread[goalId] ?? []);
|
|
5665
5688
|
return msgs.map((m) => (m.from === 'you' ? replySaidHtml(m.text)
|
|
5666
5689
|
: m.from === 'ai' ? replyAnsweredHtml(m.text)
|
|
@@ -5679,12 +5702,68 @@ function replySaidHtml(you){
|
|
|
5679
5702
|
function replyWaitHtml(label){
|
|
5680
5703
|
return `<div class="txr note"><span class="w"></span><span class="m"><span class="shimmer">${esc(label)}</span></span></div>`;
|
|
5681
5704
|
}
|
|
5705
|
+
// The settled AI voice for a `continue` reply (REVIEW-FEEDBACK-FLOW 決定2): replaces the
|
|
5706
|
+
// "Thinking…" line once the reply is picked up. A steady green dot (not a shimmer — it is
|
|
5707
|
+
// no longer waiting) beside the one-line voice; the card header turns "Working on it" too.
|
|
5708
|
+
function replyWorkingHtml(){
|
|
5709
|
+
return `<div class="txr note wkline"><span class="w"></span><span class="m"><span class="wkdot"></span>Picking this back up — I'll be back when it's ready.</span></div>`;
|
|
5710
|
+
}
|
|
5682
5711
|
function replyNoteRowHtml(body){
|
|
5683
5712
|
return `<div class="txr note"><span class="w"></span><span class="m">${body}</span></div>`;
|
|
5684
5713
|
}
|
|
5685
5714
|
function replyAnsweredHtml(text){
|
|
5686
5715
|
return `<div class="txr cl"><span class="w">Claude</span><span class="m">${esc(text)}</span></div>`;
|
|
5687
5716
|
}
|
|
5717
|
+
// ── See-all card reply thread (unified, Masa 2026-07-24) ────────────────────────────────
|
|
5718
|
+
// One per-card model instead of the tangle of a single live note + server reloads that kept
|
|
5719
|
+
// losing/reordering lines. Each card owns its conversation in CLIENT state:
|
|
5720
|
+
// it.rthread = [{ who:'you'|'ai'|'note', text, seq }] (rendered SORTED by seq, so send
|
|
5721
|
+
// order is preserved even when a reply's routing round-trip resolves late)
|
|
5722
|
+
// it.rstatus = 'thinking'|'answering'|'working'|'unsure'|null (the transient trailer)
|
|
5723
|
+
// it.rchoices = the four routes, when rstatus==='unsure'
|
|
5724
|
+
// It is never re-fetched (no reload flash) and never shared between cards (no cross-wipe).
|
|
5725
|
+
let saReplySeq = 0;
|
|
5726
|
+
function saNextSeq(){ return ++saReplySeq; }
|
|
5727
|
+
function saCardThreadHtml(it){
|
|
5728
|
+
const rows = [...(it.rthread || [])].sort((a, b) => a.seq - b.seq).map((m) =>
|
|
5729
|
+
m.who === 'you' ? replySaidHtml(m.text)
|
|
5730
|
+
: m.who === 'ai' ? replyAnsweredHtml(m.text)
|
|
5731
|
+
: replyNoteRowHtml(esc(m.text))).join('');
|
|
5732
|
+
const trailer = it.rstatus === 'thinking' ? replyWaitHtml(REPLY_WAIT_ROUTING)
|
|
5733
|
+
: it.rstatus === 'answering' ? replyWaitHtml(REPLY_WAIT_ANSWERING)
|
|
5734
|
+
: it.rstatus === 'working' ? replyWorkingHtml()
|
|
5735
|
+
: it.rstatus === 'unsure' ? (replyNoteRowHtml('Which one is this?')
|
|
5736
|
+
+ `<div class="qchips">${(it.rchoices || []).map((c) =>
|
|
5737
|
+
`<button type="button" class="qchip" data-rc="${esc(c)}" data-rcgoal="${it.goalId}">${esc(REPLY_CHOICE_LABEL[c] ?? c)}</button>`).join('')}</div>`)
|
|
5738
|
+
: '';
|
|
5739
|
+
return rows + trailer;
|
|
5740
|
+
}
|
|
5741
|
+
// Seed a card's client thread from the server-persisted conversation the FIRST time we reply
|
|
5742
|
+
// to it. Without this, a card that already has history (its client it.rthread is empty — the
|
|
5743
|
+
// normal state right after a page reload, or a card reopened later) would render only the new
|
|
5744
|
+
// line the moment you reply, and the earlier replies would vanish (Masa 2026-07-24). Seeded
|
|
5745
|
+
// rows get their seq in server order (before this reply's), so the thread reads oldest→newest.
|
|
5746
|
+
async function saEnsureThread(it){
|
|
5747
|
+
if (!it || it.rthread) return;
|
|
5748
|
+
let msgs = replyThread[it.goalId];
|
|
5749
|
+
if (msgs === undefined) {
|
|
5750
|
+
try { const j = await (await fetch(withKey(`/api/goals/${it.goalId}/messages`))).json(); msgs = (j.messages ?? []).filter((m) => m.via === 'review'); replyThread[it.goalId] = msgs; }
|
|
5751
|
+
catch { msgs = []; }
|
|
5752
|
+
}
|
|
5753
|
+
it.rthread = (msgs || []).map((m) => ({ who: (m.from === 'ai' || m.from === 'manager') ? 'ai' : 'you', text: m.text, seq: saNextSeq() }));
|
|
5754
|
+
}
|
|
5755
|
+
// Poll a goal's conversation for the next AI/manager line after `seen` (an answer, §1.3 —
|
|
5756
|
+
// there is no task to watch, it arrives as a message). Returns the text, or '' on timeout.
|
|
5757
|
+
async function saPollAnswer(goalId, seen){
|
|
5758
|
+
for (let i = 0; i < 120; i++) {
|
|
5759
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
5760
|
+
let msgs = [];
|
|
5761
|
+
try { msgs = (await (await fetch(withKey(`/api/goals/${goalId}/messages`))).json()).messages ?? []; } catch { continue; }
|
|
5762
|
+
const said = msgs.slice(seen).find((m) => m.from === 'ai' || m.from === 'manager');
|
|
5763
|
+
if (said) return said.text;
|
|
5764
|
+
}
|
|
5765
|
+
return '';
|
|
5766
|
+
}
|
|
5688
5767
|
// Sent, and nothing decided yet. Shown immediately — the wait is real (a routing
|
|
5689
5768
|
// call, then the answer), so the honest thing is to say it is thinking, not to
|
|
5690
5769
|
// pretend the reply landed somewhere it has not.
|
|
@@ -5778,7 +5857,7 @@ async function sendReviewReject(){
|
|
|
5778
5857
|
$('reviewDismiss').onclick = () => sendReviewReject();
|
|
5779
5858
|
$('reviewFixSend').onclick = () => { const t = $('reviewFixInput').value.trim(); if (t || reviewAttach.length) sendReviewDismiss(t); };
|
|
5780
5859
|
$('reviewFixInput').addEventListener('keydown', (e) => {
|
|
5781
|
-
if (e.key === 'Enter') { const t = $('reviewFixInput').value.trim(); if (t || reviewAttach.length) sendReviewDismiss(t); }
|
|
5860
|
+
if (e.key === 'Enter' && !e.isComposing) { const t = $('reviewFixInput').value.trim(); if (t || reviewAttach.length) sendReviewDismiss(t); }
|
|
5782
5861
|
});
|
|
5783
5862
|
$('reviewFixImg').onclick = () => $('reviewFixFile').click();
|
|
5784
5863
|
$('reviewFixFile').addEventListener('change', async () => {
|
|
@@ -6934,7 +7013,7 @@ function inlineFsRename(span, onCommit){
|
|
|
6934
7013
|
if (changed) onCommit?.(v);
|
|
6935
7014
|
span.removeEventListener('keydown', onKey); span.removeEventListener('keyup', onKeyUp); span.removeEventListener('blur', onBlur); };
|
|
6936
7015
|
const onKey = (ev) => { ev.stopPropagation();
|
|
6937
|
-
if (ev.key === 'Enter') { ev.preventDefault(); finish(true); span.blur(); }
|
|
7016
|
+
if (ev.key === 'Enter' && !ev.isComposing) { ev.preventDefault(); finish(true); span.blur(); }
|
|
6938
7017
|
else if (ev.key === 'Escape') { ev.preventDefault(); finish(false); span.blur(); } };
|
|
6939
7018
|
const onKeyUp = (ev) => { ev.stopPropagation(); };
|
|
6940
7019
|
const onBlur = () => finish(true);
|
|
@@ -8779,7 +8858,10 @@ function saWorkflowHtml(it){
|
|
|
8779
8858
|
return '<div class="rwf">' + steps.map(([s, st]) => `<div class="wf-s ${st}"><span class="mk">${st === 'ok' ? '✓' : st === 'now' ? '→' : '□'}</span>${s}</div>`).join('') + '</div>';
|
|
8780
8859
|
}
|
|
8781
8860
|
function saItemHtml(it, i){
|
|
8782
|
-
|
|
8861
|
+
// st===3 is NOT a terminal row (REVIEW-FEEDBACK-FLOW 決定3): a reply is not a dismissal.
|
|
8862
|
+
// The old sa-done "feedback returned to To Do" line is gone; st===3 now renders the full
|
|
8863
|
+
// card in its "Working on it" state (see `working` below). Terminal rows: 1/2/4/5 only.
|
|
8864
|
+
const done = { 1: ['ok', '✓', 'Approved'], 2: ['no', '↩', 'Dismissed'], 4: ['ar', '—', 'Archived'], 5: ['no', '↩', 'Reverted — change undone'] }[it.st];
|
|
8783
8865
|
if (done) {
|
|
8784
8866
|
// Revert deliberately has NO undo: the PR is closed and its branch deleted —
|
|
8785
8867
|
// an "UNDO" here would be a lie (HANDOFF-v45 §5.3 honesty over symmetry).
|
|
@@ -9004,8 +9086,17 @@ function saItemHtml(it, i){
|
|
|
9004
9086
|
? `<section class="rsec rconv"><div class="rsec-h">Conversation</div><details class="lacc" data-detkey="${gk}:msg" data-msgs="${it.goalId}"${msgOpen}><summary><i class="ltog">›</i><div class="rtx lfirst">${msgSummary}</div></summary><div class="lbody"><div class="rtx" data-msgbody="${it.goalId}">${msgBody}</div></div></details></section>`
|
|
9005
9087
|
: '';
|
|
9006
9088
|
const S = (label, body) => body ? `<section class="rsec"><div class="rsec-h">${label}</div>${body}</section>` : '';
|
|
9007
|
-
|
|
9008
|
-
|
|
9089
|
+
// st===3 = a reply was sent and the goal is being picked back up (REVIEW-FEEDBACK-FLOW
|
|
9090
|
+
// 決定1-4). The card is NOT collapsed: it stays open and the header reads "Working on it".
|
|
9091
|
+
// Your reply + the AI's one-line voice ("Picking this back up…") live in the reply thread
|
|
9092
|
+
// BELOW the box (the same .replynote surface the answer flow uses) — so sending only APPENDS
|
|
9093
|
+
// a line and never re-lays-out the card. It leaves Review only when the card is closed.
|
|
9094
|
+
const working = it.st === 3;
|
|
9095
|
+
const rst = working
|
|
9096
|
+
? '<span class="rst working"><span class="rdot"></span>Working on it</span>'
|
|
9097
|
+
: '<span class="rst"><span class="rdot"></span>In review</span>';
|
|
9098
|
+
return `<div class="sa-item lg redesign${it.goalId === SA.cur ? ' cur' : ''}${working ? ' working' : ''}" data-gid="${it.goalId}">
|
|
9099
|
+
<div class="rhead"><span class="rtag">${it.no != null ? `#${it.no}` : ''}</span>${rst}</div>
|
|
9009
9100
|
<h2 class="rtitle">${esc(it.title)}</h2>
|
|
9010
9101
|
${meta}
|
|
9011
9102
|
${reqSec}
|
|
@@ -9149,7 +9240,10 @@ function saSync(){
|
|
|
9149
9240
|
const rows = saRows();
|
|
9150
9241
|
for (const row of rows) {
|
|
9151
9242
|
const ex = SA.items.find((x) => x.goalId === row.goalId);
|
|
9152
|
-
|
|
9243
|
+
// Preserve st===3 ("Working on it", 決定1-4): a reply held the card open in place; if a
|
|
9244
|
+
// tick still saw it as 'review' before the /dismiss→running upsert landed, resetting st
|
|
9245
|
+
// would snap the card back to "In review". Terminal verdicts (1/2/4/5) reset as before.
|
|
9246
|
+
if (ex) { const st = ex.st, inflight = ex.inflight; Object.assign(ex, row); ex.st = (st > 0 && st !== 3 && !inflight) ? 0 : st; }
|
|
9153
9247
|
else SA.items.push({ ...row, st: 0 });
|
|
9154
9248
|
}
|
|
9155
9249
|
saRender();
|
|
@@ -9230,49 +9324,111 @@ async function saCall(ids, path, body){
|
|
|
9230
9324
|
}
|
|
9231
9325
|
return r;
|
|
9232
9326
|
}
|
|
9233
|
-
// The review card's reply box (§1.3/§1.4).
|
|
9234
|
-
//
|
|
9235
|
-
//
|
|
9236
|
-
//
|
|
9237
|
-
// To Do" is a lie when the reply was a question, and an optimistic lie is worse
|
|
9238
|
-
// than a moment of waiting: the card would say the work was sent back while the
|
|
9239
|
-
// goal sat untouched in review.
|
|
9327
|
+
// The review card's reply box (§1.3/§1.4). The reply is routed by the AI itself
|
|
9328
|
+
// (outcome:'auto', ONE-CONVERSATION §1.4) — this layer never guesses continue vs
|
|
9329
|
+
// question. Unlike saAct it does NOT show its outcome first: "Feedback sent" would
|
|
9330
|
+
// be a lie when the reply was a question.
|
|
9240
9331
|
async function saReplyCall(it, text, outcome){
|
|
9241
9332
|
const r = await saCall(it.ids, 'dismiss', { text, outcome });
|
|
9242
9333
|
if (!r?.ok) throw new Error('reply failed');
|
|
9243
9334
|
return r.json().catch(() => null);
|
|
9244
9335
|
}
|
|
9336
|
+
function saCardEl(goalId){ return document.querySelector(`#seeall .sa-item[data-gid="${goalId}"]`); }
|
|
9337
|
+
// Flip one card to its "Working on it" head without redrawing anything else (決定2).
|
|
9338
|
+
function saFlipWorking(goalId){
|
|
9339
|
+
const card = saCardEl(goalId);
|
|
9340
|
+
if (!card) return;
|
|
9341
|
+
card.classList.add('working');
|
|
9342
|
+
const rst = card.querySelector('.rhead .rst');
|
|
9343
|
+
if (rst) { rst.className = 'rst working'; rst.innerHTML = '<span class="rdot"></span>Working on it'; }
|
|
9344
|
+
}
|
|
9345
|
+
// Repaint one card's reply thread IN PLACE from its client state — no saRender, so nothing
|
|
9346
|
+
// above the box moves. Header is flipped to "Working on it" iff the card is in that state,
|
|
9347
|
+
// and the cursor is kept in this box (a blurred box would let an SSE tick fire a full
|
|
9348
|
+
// saRender = a flash). Everything the reply UI does routes through here.
|
|
9349
|
+
function saPaintCard(goalId){
|
|
9350
|
+
const it = SA.items.find((x) => x.goalId === goalId);
|
|
9351
|
+
if (it?.rstatus === 'working') saFlipWorking(goalId);
|
|
9352
|
+
paintReplyNote(); // clears + repaints every card's thread via paintSettledThreads
|
|
9353
|
+
saCardEl(goalId)?.querySelector('.sa-chat')?.focus({ preventScroll: true });
|
|
9354
|
+
}
|
|
9355
|
+
// A reply is "one more line in the conversation" (REVIEW-FEEDBACK-FLOW 決定1-4), NOT a verdict.
|
|
9356
|
+
// Every reply is appended to the card's OWN client thread (it.rthread) in send order and shown
|
|
9357
|
+
// in place — no saRender (no flash/shift), no shared live note (no cross-card wipe), no lost
|
|
9358
|
+
// line (a follow-up while the AI is working goes via /reply, which accepts a running goal, so
|
|
9359
|
+
// it never 409s). The FIRST reply on a fresh review card is routed by the AI (answer / unsure /
|
|
9360
|
+
// continue, §1.3/§1.4); a follow-up on a card already in conversation is always another
|
|
9361
|
+
// instruction (continue).
|
|
9245
9362
|
async function saSendReply(it, i, text){
|
|
9246
9363
|
const goalId = it.goalId;
|
|
9247
|
-
//
|
|
9248
|
-
//
|
|
9249
|
-
|
|
9364
|
+
await saEnsureThread(it); // fold in any existing history BEFORE this reply (so it isn't hidden)
|
|
9365
|
+
const seq = saNextSeq(); // reserve send order NOW, so a late-resolving route keeps its place
|
|
9366
|
+
// Clear the box in place — keep it FOCUSED, not blurred (saSync skips a rebuild while an
|
|
9367
|
+
// input in #seeall has focus, which is what keeps an SSE tick from flashing the panel).
|
|
9368
|
+
document.querySelectorAll(`#seeall .sa-item[data-gid="${goalId}"] .sa-chat`).forEach((inp) => { inp.value = ''; });
|
|
9369
|
+
// Append the line to the card's own thread immediately — this is what the person sees, and
|
|
9370
|
+
// its `seq` fixes its order regardless of when any round trip returns.
|
|
9371
|
+
it.rthread = [...(it.rthread || []), { who: 'you', text, seq }];
|
|
9372
|
+
const followUp = it.st === 3 || it.inflight; // card already in conversation → another instruction
|
|
9373
|
+
|
|
9374
|
+
if (followUp) {
|
|
9375
|
+
it.st = 3; it.rstatus = 'working'; SA.cur = goalId;
|
|
9376
|
+
saPaintCard(goalId);
|
|
9377
|
+
try {
|
|
9378
|
+
const r = await fetch(withKey(`/api/goals/${goalId}/reply`), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ text }) });
|
|
9379
|
+
if (!r.ok) throw new Error('reply failed');
|
|
9380
|
+
} catch {
|
|
9381
|
+
it.rthread = it.rthread.filter((m) => m.seq !== seq); // roll back just this line
|
|
9382
|
+
if (!it.rthread.some((m) => m.who === 'you')) { it.st = 0; it.rstatus = null; }
|
|
9383
|
+
saPaintCard(goalId); showErr('Reply failed.');
|
|
9384
|
+
}
|
|
9385
|
+
return;
|
|
9386
|
+
}
|
|
9387
|
+
|
|
9388
|
+
// First reply: route it. Show "Thinking…" under the line while the AI decides.
|
|
9250
9389
|
const seen = state.goals.find((g) => g.id === goalId)?.messageCount ?? 0;
|
|
9251
|
-
it.inflight = true;
|
|
9252
|
-
|
|
9390
|
+
it.inflight = true; it.rstatus = 'thinking'; SA.cur = goalId;
|
|
9391
|
+
saPaintCard(goalId);
|
|
9253
9392
|
try {
|
|
9254
9393
|
let body = await saReplyCall(it, text, 'auto');
|
|
9255
9394
|
if (body?.outcome === 'unsure') {
|
|
9256
|
-
it.inflight = false;
|
|
9257
|
-
|
|
9258
|
-
|
|
9259
|
-
it.
|
|
9395
|
+
it.inflight = false; it.rstatus = 'unsure'; it.rchoices = body.choices || REPLY_OUTCOMES_CLIENT;
|
|
9396
|
+
saPaintCard(goalId);
|
|
9397
|
+
const picked = await new Promise((resolve) => { it._routeResolve = resolve; });
|
|
9398
|
+
it._routeResolve = null;
|
|
9399
|
+
if (!picked) { // cancelled: drop this line, revert the card
|
|
9400
|
+
it.rthread = it.rthread.filter((m) => m.seq !== seq); it.rstatus = null;
|
|
9401
|
+
if (!it.rthread.length) it.st = 0;
|
|
9402
|
+
saPaintCard(goalId); return;
|
|
9403
|
+
}
|
|
9404
|
+
it.inflight = true; it.rstatus = 'thinking'; saPaintCard(goalId);
|
|
9260
9405
|
body = await saReplyCall(it, text, picked);
|
|
9261
9406
|
}
|
|
9262
9407
|
it.inflight = false;
|
|
9263
|
-
if (body?.outcome === 'answer') {
|
|
9264
|
-
|
|
9265
|
-
|
|
9266
|
-
|
|
9267
|
-
|
|
9408
|
+
if (body?.outcome === 'answer') { // a question: the AI answers, the goal never moves
|
|
9409
|
+
it.rstatus = 'answering'; saPaintCard(goalId);
|
|
9410
|
+
const ans = await saPollAnswer(goalId, seen + 1);
|
|
9411
|
+
it.rthread = [...it.rthread, { who: 'ai', text: ans || 'No answer came back.', seq: saNextSeq() }];
|
|
9412
|
+
it.rstatus = null; saPaintCard(goalId);
|
|
9268
9413
|
return;
|
|
9269
9414
|
}
|
|
9270
|
-
|
|
9415
|
+
// continue: the card is now "Working on it".
|
|
9416
|
+
it.st = 3; it.rstatus = 'working'; SA.cur = goalId;
|
|
9417
|
+
saPaintCard(goalId);
|
|
9271
9418
|
} catch {
|
|
9272
|
-
it.inflight = false;
|
|
9273
|
-
|
|
9419
|
+
it.inflight = false;
|
|
9420
|
+
it.rthread = it.rthread.filter((m) => m.seq !== seq);
|
|
9421
|
+
if (!it.rthread.length) it.rstatus = null;
|
|
9422
|
+
saPaintCard(goalId); showErr('Action failed.');
|
|
9274
9423
|
}
|
|
9275
9424
|
}
|
|
9425
|
+
// The unsure chips resolve the pending route (see saSendReply's Promise). Kept keyed by goal
|
|
9426
|
+
// so a repaint (which re-creates the buttons) still lands on the right pending reply.
|
|
9427
|
+
const REPLY_OUTCOMES_CLIENT = ['continue', 'rescope', 'split', 'answer'];
|
|
9428
|
+
function saPickRoute(goalId, route){
|
|
9429
|
+
const it = SA.items.find((x) => x.goalId === goalId);
|
|
9430
|
+
if (it?._routeResolve) it._routeResolve(route);
|
|
9431
|
+
}
|
|
9276
9432
|
// one action round-trip: optimistic one-liner → endpoint → settle or roll back
|
|
9277
9433
|
async function saAct(it, st, path, body){
|
|
9278
9434
|
it.st = st; it.inflight = true; saRender();
|
|
@@ -9586,7 +9742,7 @@ $('ctxdirmenu').addEventListener('click', (e) => {
|
|
|
9586
9742
|
});
|
|
9587
9743
|
$('ctxdirmenu').addEventListener('keydown', (e) => {
|
|
9588
9744
|
e.stopPropagation();
|
|
9589
|
-
if (e.key === 'Enter' && e.target.id === 'ctxdirin') { e.preventDefault(); setProjectFolder(e.target.value); }
|
|
9745
|
+
if (e.key === 'Enter' && !e.isComposing && e.target.id === 'ctxdirin') { e.preventDefault(); setProjectFolder(e.target.value); }
|
|
9590
9746
|
});
|
|
9591
9747
|
$('appbtn').onclick = (e) => { e.stopPropagation(); $('appmenu').classList.toggle('show'); };
|
|
9592
9748
|
$('appmenu').addEventListener('click', (e) => {
|
package/engine/lib.mjs
CHANGED
|
@@ -1158,6 +1158,49 @@ export function isInconclusiveTestRun({ ran = false, timedOut = false, passed =
|
|
|
1158
1158
|
return Boolean(ran) && Boolean(timedOut) && !realAssertion && passed === 0;
|
|
1159
1159
|
}
|
|
1160
1160
|
|
|
1161
|
+
// Infra failures the review gate must NOT block real work on. A test that stands
|
|
1162
|
+
// up a real server/headless Chrome fails these ways under WHOLE-MACHINE
|
|
1163
|
+
// saturation (prod :4400 + several agent sessions + the baseline run all live at
|
|
1164
|
+
// once — which --test-concurrency=1 inside one suite cannot prevent, the load is
|
|
1165
|
+
// from OTHER processes): the server dies during boot ("server exited (code N)
|
|
1166
|
+
// before it was ready"), misses its window ("did not report ready in time"), or a
|
|
1167
|
+
// puppeteer wait/navigation times out. None is the goal's code regressing, and
|
|
1168
|
+
// they SHUFFLE to a different set of tests each run, so classifyTestGate's
|
|
1169
|
+
// name-diff can never subtract them. Count them so a run can discount them.
|
|
1170
|
+
export function countInfraFlakes(text = '') {
|
|
1171
|
+
const pats = [
|
|
1172
|
+
/server exited \(code \d+\) before it was ready/g, // test helper: server crashed on boot under load
|
|
1173
|
+
/did not report ready in time/g, // test helper: server missed its startup window
|
|
1174
|
+
/Waiting failed: \d+\s*ms exceeded/g, // puppeteer waitForFunction/Selector
|
|
1175
|
+
/Navigation timeout of \d+\s*ms exceeded/g, // puppeteer page.goto
|
|
1176
|
+
/test timed out after \d+\s*ms/g, // node --test per-test timeout
|
|
1177
|
+
/\bTarget closed\b/g, // puppeteer: browser died mid-test
|
|
1178
|
+
];
|
|
1179
|
+
return pats.reduce((n, re) => n + (String(text ?? '').match(re) || []).length, 0);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// Decide how many of a test run's failures are REAL (should block Review) vs
|
|
1183
|
+
// infra flakes (countInfraFlakes above). The old runTestsOnce let a SINGLE
|
|
1184
|
+
// "expected … to" match anywhere in megabytes of output (realAssertion=true)
|
|
1185
|
+
// disable the infra discount for ALL failures — which is exactly why 93 boot
|
|
1186
|
+
// failures ("server exited (code 1) before it was ready", 650 passed) blocked
|
|
1187
|
+
// goals #616/#620 (2026-07-24): every failure was infra, yet realAssertion
|
|
1188
|
+
// tripped on stray text and none was discounted.
|
|
1189
|
+
// - infraOnly: every counted failure is an infra flake (infraFlakes covers the
|
|
1190
|
+
// whole count) → block on none, regardless of a stray assertion string. If a
|
|
1191
|
+
// REAL assertion failure existed it would be an ADDITIONAL failure not covered
|
|
1192
|
+
// by the infra count, so infraFlakes < failedTotal and we fall through.
|
|
1193
|
+
// - a real assertion sharing a mixed run → fail CLOSED on the full count.
|
|
1194
|
+
// - otherwise → discount the infra flakes from the total.
|
|
1195
|
+
export function classifyRunFailures({ failedTotal = 0, infraFlakes = 0, realAssertion = false } = {}) {
|
|
1196
|
+
const ft = Math.max(0, Number(failedTotal) || 0);
|
|
1197
|
+
const inf = Math.max(0, Number(infraFlakes) || 0);
|
|
1198
|
+
if (ft <= 0) return { effectiveFailed: 0, infraOnly: false };
|
|
1199
|
+
if (inf >= ft) return { effectiveFailed: 0, infraOnly: true };
|
|
1200
|
+
if (realAssertion) return { effectiveFailed: ft, infraOnly: false };
|
|
1201
|
+
return { effectiveFailed: Math.max(0, ft - inf), infraOnly: false };
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1161
1204
|
// The verification-gate decision (Masa 2026-07-19: "検証/proof は Failed を作らない").
|
|
1162
1205
|
// PROOF IS ADVISORY: a UI goal whose screenshot could not be captured
|
|
1163
1206
|
// (proofMissing) or whose pixel heuristic could not auto-confirm the target
|
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 } 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, buildEphemeralSeedLog, buildEphemeralSeedProjects, trimTaskActivityForState, buildAskContext, WORKER_TOOLS, workerPrompt, verifyCfAccessJwt, resolveIdentity, goalVisibleTo, clampParallelLimit, canStartMore, nextRunnableTasks, goalsConflict, detectConflicts, pickFoldTarget, hasTestRelevantChanges, parseFailingTestNames, classifyTestGate, isInconclusiveTestRun, classifyVerifyGate, buildExecutionPlan, buildContextHandoffSummary, buildFailurePostmortem, appendFailureMemory, latestFailurePolicy, classifyChangeRisk, checkRunBudget, usageBudgetTokens, isRateLimited, nextResumeDelay, checkFreeTierLimit, resolveEntitlement, resolveCachedEntitlement, FREE_TIER_LIMITS, QUEUED_GOAL_STATUSES, verifyLicenseToken, 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, 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, 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';
|
|
25
25
|
import { createSerialQueue } from './lib.mjs';
|
|
26
26
|
import { collectProjectRules } from './lib.mjs';
|
|
27
27
|
import { openPR } from './pr.mjs';
|
|
@@ -2475,15 +2475,21 @@ function runTestsOnce(dir) {
|
|
|
2475
2475
|
const n = (re) => { const m = text.match(re); return m ? Number(m[1]) : null; };
|
|
2476
2476
|
const passed = n(/ℹ pass (\d+)/) ?? n(/# pass (\d+)/) ?? n(/(\d+) passing/) ?? 0;
|
|
2477
2477
|
const failedTotal = n(/ℹ fail (\d+)/) ?? n(/# fail (\d+)/) ?? n(/(\d+) failing/) ?? (e ? 1 : 0);
|
|
2478
|
-
// Don't block on INFRA flakes: the integration suite spawns real servers
|
|
2479
|
-
// and under
|
|
2480
|
-
//
|
|
2481
|
-
//
|
|
2482
|
-
//
|
|
2483
|
-
//
|
|
2484
|
-
|
|
2478
|
+
// Don't block on INFRA flakes: the integration suite spawns real servers +
|
|
2479
|
+
// headless Chrome, and under WHOLE-MACHINE saturation (prod :4400 + several
|
|
2480
|
+
// agent sessions + the baseline run) a server dies on boot ("server exited
|
|
2481
|
+
// (code N) before it was ready"), misses its window ("did not report ready
|
|
2482
|
+
// in time"), or a puppeteer wait times out — none a code regression, and
|
|
2483
|
+
// they shuffle to different tests each run. countInfraFlakes tallies them;
|
|
2484
|
+
// classifyRunFailures decides how many failures are REAL. Fail CLOSED: a
|
|
2485
|
+
// real AssertionError sharing a MIXED run still blocks (infraFlakes <
|
|
2486
|
+
// failedTotal). Only when EVERY failure is infra (infraOnly) do we discount
|
|
2487
|
+
// despite a stray "expected … to" match — the coarse global realAssertion
|
|
2488
|
+
// flag is exactly what stranded #616/#620 (93 boot failures, all discountable,
|
|
2489
|
+
// yet realAssertion tripped on unrelated text and blocked them).
|
|
2490
|
+
const infraFlakes = countInfraFlakes(text);
|
|
2485
2491
|
const realAssertion = /AssertionError|Expected values to be|expected .+ (?:to |but )/i.test(text);
|
|
2486
|
-
const
|
|
2492
|
+
const { effectiveFailed: failed, infraOnly } = classifyRunFailures({ failedTotal, infraFlakes, realAssertion });
|
|
2487
2493
|
// Was the run KILLED at the execFile timeout (machine too saturated to
|
|
2488
2494
|
// finish), rather than exiting on its own? execFile sets killed=true and
|
|
2489
2495
|
// signal='SIGTERM' on timeout. That is the inconclusive signal — a normal
|
|
@@ -2492,7 +2498,7 @@ function runTestsOnce(dir) {
|
|
|
2492
2498
|
const timedOut = Boolean(e && (e.killed || e.signal === 'SIGTERM' || e.code === 'ETIMEDOUT'));
|
|
2493
2499
|
// Capture the failing test NAMES too (not just the count) so the gate can
|
|
2494
2500
|
// diff them against the base's failures and block only on NEW ones.
|
|
2495
|
-
res({ passed, failed, timedOut, realAssertion, detail: testOutputExcerpt(text), failingNames: parseFailingTestNames(text) });
|
|
2501
|
+
res({ passed, failed, timedOut, realAssertion, infraOnly, infraFlakes, detail: testOutputExcerpt(text), failingNames: parseFailingTestNames(text) });
|
|
2496
2502
|
});
|
|
2497
2503
|
}));
|
|
2498
2504
|
}
|
|
@@ -2516,11 +2522,16 @@ async function runProjectTests(dir) {
|
|
|
2516
2522
|
// a real red in either run still fails closed.
|
|
2517
2523
|
timedOut: Boolean(r.timedOut && r2.timedOut),
|
|
2518
2524
|
realAssertion: r.realAssertion || r2.realAssertion,
|
|
2525
|
+
// infra state from the run we keep (the one with fewer real failures): if
|
|
2526
|
+
// its failures were all infra flakes (infraOnly → failed 0), that is what
|
|
2527
|
+
// the gate should see, so no good goal is blocked on a saturated boot.
|
|
2528
|
+
infraOnly: !!kept.infraOnly,
|
|
2529
|
+
infraFlakes: kept.infraFlakes ?? 0,
|
|
2519
2530
|
detail: kept.detail || r.detail || r2.detail || '',
|
|
2520
2531
|
failingNames: kept.failingNames ?? r.failingNames ?? [],
|
|
2521
2532
|
};
|
|
2522
2533
|
}
|
|
2523
|
-
return { ran: true, passed: r.passed, failed: r.failed, ok: r.failed === 0, timedOut: !!r.timedOut, realAssertion: !!r.realAssertion, detail: r.detail ?? '', failingNames: r.failingNames ?? [] };
|
|
2534
|
+
return { ran: true, passed: r.passed, failed: r.failed, ok: r.failed === 0, timedOut: !!r.timedOut, realAssertion: !!r.realAssertion, infraOnly: !!r.infraOnly, infraFlakes: r.infraFlakes ?? 0, detail: r.detail ?? '', failingNames: r.failingNames ?? [] };
|
|
2524
2535
|
}
|
|
2525
2536
|
|
|
2526
2537
|
// Failing tests on the BASE (pre-change) checkout — the set the review gate
|
|
@@ -2982,6 +2993,15 @@ async function verifyGate(goal, prProject, siblings, baseProject, activityTask =
|
|
|
2982
2993
|
goal.testResult.inconclusive = true;
|
|
2983
2994
|
goalAct('Project tests could not complete — the suite was killed before finishing (0 passed, no real assertion), which means the machine was too saturated to run them, not a code failure. Treating as inconclusive, not blocking. Re-run the tests when the machine is idle to confirm.');
|
|
2984
2995
|
}
|
|
2996
|
+
// Infra-only run (root fix for #616/#620, 2026-07-24): the suite finished but
|
|
2997
|
+
// EVERY failure was an infra flake (a real server could not boot — "server
|
|
2998
|
+
// exited (code N) before it was ready" — or a wait timed out under whole-machine
|
|
2999
|
+
// saturation), so runProjectTests already discounted them to 0. testsFailing is
|
|
3000
|
+
// therefore already false; surface it as an advisory so the human knows the
|
|
3001
|
+
// suite couldn't be trusted here (not that it ran green), and can re-run idle.
|
|
3002
|
+
if (goal.testResult.ran && goal.testResult.infraOnly && !goal.testResult.inconclusive) {
|
|
3003
|
+
goalAct(`Project tests: all ${goal.testResult.infraFlakes || goal.testResult.failed || ''} failure(s) were infra flakes (a real server could not boot / a wait timed out under machine load), not code regressions — not blocking. Re-run when the machine is idle to confirm.`.replace(/\s{2,}/g, ' '));
|
|
3004
|
+
}
|
|
2985
3005
|
// Baseline diff (Masa 2026-07-19): block ONLY on failures THIS change
|
|
2986
3006
|
// introduced. A test already red on the base — e.g. persist-failure on clean
|
|
2987
3007
|
// origin/main — is pre-existing noise and must not knock a proof-verified
|
package/package.json
CHANGED