@galda/cli 0.10.42 → 0.10.44
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 +118 -37
- package/engine/lib.mjs +72 -1
- package/engine/server.mjs +92 -15
- package/package.json +1 -1
package/app/index.html
CHANGED
|
@@ -318,6 +318,12 @@
|
|
|
318
318
|
.ctxmenu .ctxmsg{padding:2px 5px 3px;font-size:11px;color:var(--ink3)}
|
|
319
319
|
.ctxmenu .ctxmsg:empty{display:none}
|
|
320
320
|
.ctxmenu .ctxmsg.bad{color:var(--danger)}
|
|
321
|
+
/* "Open folder…" — the Claude-Code entry that opens the OS folder chooser on a
|
|
322
|
+
local session. A single-line row (the repo rows above are 2-line name+path),
|
|
323
|
+
hairline-separated from the detected list, matching .appopt grammar. */
|
|
324
|
+
.ctxmenu .appopt.ctxopen{flex-direction:row;align-items:center;gap:7px;border-top:1px solid var(--hair);margin-top:4px;padding-top:8px}
|
|
325
|
+
.ctxmenu .appopt.ctxopen svg{width:14px;height:14px;flex:0 0 14px;stroke:currentColor;stroke-width:1.7;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
326
|
+
.ctxmenu .appopt.ctxopen .nm{color:var(--ink);font-size:12.5px;line-height:1.3}
|
|
321
327
|
.appopt{display:flex;align-items:center;gap:7px;height:28px;padding:0 8px;border:0;border-radius:6px;background:transparent;
|
|
322
328
|
color:var(--ink2);font:inherit;font-size:12px;cursor:pointer;text-align:left;width:100%}
|
|
323
329
|
.appopt:hover{background:rgba(var(--tint),.06);color:var(--ink)}
|
|
@@ -1018,29 +1024,41 @@
|
|
|
1018
1024
|
/* Goal review detail panel (task 72): tapping any Review-column goal item
|
|
1019
1025
|
(whether or not it has a PR) opens this instead of the chat panel —
|
|
1020
1026
|
Requirement / What was done / Proof, then Approve/Dismiss + revision chat. */
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
.
|
|
1028
|
-
|
|
1027
|
+
/* View-log / Attention modal — transcribes the Review card's redesign grammar
|
|
1028
|
+
(REVIEW-CARD-SPEC / .redesign): panel container, 40px Linear-docs gutters,
|
|
1029
|
+
sentence-case UI section heads, 14px --ink body prose, canvas log box. Same
|
|
1030
|
+
IDs/classes/markup as before — ONLY the look changes. Behaviour is unchanged
|
|
1031
|
+
(Approve stays hidden for failed; failure brief / Archive / send-back shown).
|
|
1032
|
+
All tokens (--panel/--hair/--ink*/--canvas/--ui/--mono) so it adapts per theme. */
|
|
1033
|
+
.gdoverlay .modal{width:min(680px,94%);max-height:min(86vh,760px);display:flex;flex-direction:column;position:relative;overflow:hidden;
|
|
1034
|
+
background:var(--panel);border:0;border-radius:18px;padding:0;
|
|
1035
|
+
box-shadow:0 18px 54px -18px rgba(15,17,40,.30),0 0 0 1px var(--hair);
|
|
1036
|
+
backdrop-filter:blur(30px) saturate(1.4);-webkit-backdrop-filter:blur(30px) saturate(1.4)}
|
|
1037
|
+
.gdclosex{position:absolute;right:18px;top:16px;z-index:2}
|
|
1038
|
+
.gdoverlay #gdTitle{margin:0;padding:28px 40px 0;font-family:var(--ui);font-size:18px;font-weight:680;letter-spacing:-.018em;line-height:1.3;color:var(--ink)}
|
|
1039
|
+
.gdscroll{flex:1;overflow:auto;min-height:0;padding:0 34px 4px 40px}
|
|
1040
|
+
.gdsec{margin-top:24px}
|
|
1041
|
+
.gdsec:first-of-type{margin-top:20px}
|
|
1042
|
+
/* section head = Linear-docs sentence-case (markup text already "Requirement"/"What
|
|
1043
|
+
was done"/"Proof"/"Log"), matching .redesign .rsec-h — not the old mono eyebrow. */
|
|
1044
|
+
.gdlabel{font-family:var(--ui);font-size:15px;font-weight:680;letter-spacing:-.012em;line-height:1.3;color:var(--ink);text-transform:none;margin-bottom:9px}
|
|
1045
|
+
.gdrequirement{font-size:14px;line-height:1.62;letter-spacing:-.012em;color:var(--ink);max-width:70ch;white-space:pre-wrap}
|
|
1046
|
+
.gdbrief{display:none;margin:16px 0 2px;border:0;border-radius:10px;background:color-mix(in srgb,var(--ink) 4%,transparent);padding:12px 14px}
|
|
1029
1047
|
.gdbrief.show{display:block}
|
|
1030
|
-
.gdbrief .brow{display:grid;grid-template-columns:120px 1fr;gap:12px;padding:
|
|
1048
|
+
.gdbrief .brow{display:grid;grid-template-columns:120px 1fr;gap:12px;padding:9px 0;border-top:1px solid var(--hair)}
|
|
1031
1049
|
.gdbrief .brow:first-child{border-top:0;padding-top:0}
|
|
1032
|
-
.gdbrief .bk{font-family:var(--
|
|
1033
|
-
.gdbrief .bv{font-size:
|
|
1034
|
-
.gdtask{margin-bottom:
|
|
1050
|
+
.gdbrief .bk{font-family:var(--ui);font-size:12px;font-weight:600;letter-spacing:-.004em;text-transform:none;color:var(--ink3)}
|
|
1051
|
+
.gdbrief .bv{font-size:14px;line-height:1.6;letter-spacing:-.012em;color:var(--ink)}
|
|
1052
|
+
.gdtask{margin-bottom:12px}
|
|
1035
1053
|
.gdtask:last-child{margin-bottom:0}
|
|
1036
|
-
.gdtasktitle{font-size:
|
|
1037
|
-
.gdtaskresult{font-size:
|
|
1038
|
-
.gdlog{max-height:min(34vh,280px);overflow:auto;border:
|
|
1039
|
-
font-family:var(--mono);font-size:10.5px;line-height:1.
|
|
1054
|
+
.gdtasktitle{font-size:14px;font-weight:600;letter-spacing:-.012em;color:var(--ink)}
|
|
1055
|
+
.gdtaskresult{font-size:14px;color:var(--ink2);line-height:1.62;letter-spacing:-.012em;white-space:pre-wrap;margin-top:4px;max-width:70ch}
|
|
1056
|
+
.gdlog{max-height:min(34vh,280px);overflow:auto;border:0;border-radius:9px;background:var(--canvas);padding:10px 12px;
|
|
1057
|
+
font-family:var(--mono);font-size:10.5px;line-height:1.75;color:var(--ink2);white-space:pre-wrap}
|
|
1040
1058
|
.gdlogline{border-top:1px solid var(--hair);padding-top:6px;margin-top:6px}
|
|
1041
1059
|
.gdlogline:first-child{border-top:0;padding-top:0;margin-top:0}
|
|
1042
|
-
.gdempty{font-size:
|
|
1043
|
-
.gdoverlay .sa-act{border-top:1px solid var(--hair);padding
|
|
1060
|
+
.gdempty{font-size:13px;color:var(--ink3)}
|
|
1061
|
+
.gdoverlay .sa-act{border-top:1px solid var(--hair);padding:14px 40px 28px;margin-top:20px}
|
|
1044
1062
|
.more{margin:6px auto 4px;padding:5px 18px;width:fit-content;display:block;text-align:center;color:var(--ink3);font-size:11.5px;
|
|
1045
1063
|
border:1px solid var(--hair2);border-radius:999px;background:var(--glass);backdrop-filter:blur(10px);cursor:pointer}
|
|
1046
1064
|
.more:hover{color:var(--ink)}
|
|
@@ -2176,6 +2194,13 @@
|
|
|
2176
2194
|
.redesign .rask,.redesign .rcheck,.redesign .rcheck li,.redesign .rcheck1{color:var(--ink) !important}
|
|
2177
2195
|
.redesign ul.rcheck{margin:2px 0 0;padding:0 0 0 18px;font-size:14px;line-height:1.7;letter-spacing:-.012em}
|
|
2178
2196
|
.redesign ul.rcheck li{margin-bottom:6px}
|
|
2197
|
+
/* Also carries — each folded goal, with Open / Undo (restore). The controls sit at the
|
|
2198
|
+
row end; muted mono buttons, glyph-coloured hover only (§5.5), no filled surface. */
|
|
2199
|
+
.redesign ul.rcarries li{display:flex;align-items:center;gap:10px}
|
|
2200
|
+
.redesign ul.rcarries .cft{min-width:0;overflow:hidden;text-overflow:ellipsis}
|
|
2201
|
+
.redesign ul.rcarries .cfa{margin-left:auto;display:inline-flex;gap:5px;flex:0 0 auto}
|
|
2202
|
+
.redesign ul.rcarries .cfb{height:22px;padding:0 9px;border:1px solid var(--hair2);border-radius:7px;background:transparent;color:var(--ink2);font-family:var(--mono);font-size:10px;cursor:pointer}
|
|
2203
|
+
.redesign ul.rcarries .cfb:hover{background:var(--hover);color:var(--ink)}
|
|
2179
2204
|
/* Activity (spec §3) = ONE Linear-docs pulldown. Heading plain; chevron on the FIRST
|
|
2180
2205
|
line; tapping opens the rest below, offset-indented (no left rule); Discussion folds
|
|
2181
2206
|
in at the bottom of the opened body. */
|
|
@@ -3118,7 +3143,7 @@
|
|
|
3118
3143
|
<div class="modal reviewmodal">
|
|
3119
3144
|
<div class="revcount" id="reviewCount">REVIEW</div>
|
|
3120
3145
|
<h3 id="reviewGoalTitle">Review</h3>
|
|
3121
|
-
<p>Review each requirement with its capture and approve it individually — approved ones fold away. Or approve the whole goal below to send it straight to Done; the next review pops in automatically. Tap outside to close.</p>
|
|
3146
|
+
<p id="reviewIntro">Review each requirement with its capture and approve it individually — approved ones fold away. Or approve the whole goal below to send it straight to Done; the next review pops in automatically. Tap outside to close.</p>
|
|
3122
3147
|
<div class="rvtoast" id="reviewPrevToast" style="display:none"></div>
|
|
3123
3148
|
<div class="revsummary" id="reviewSummary">0 / 0 approved</div>
|
|
3124
3149
|
<div class="revdone" id="reviewDoneMsg">
|
|
@@ -4723,7 +4748,7 @@ function renderTasks(){
|
|
|
4723
4748
|
}
|
|
4724
4749
|
for (const inp of $('tasklist').querySelectorAll('[data-qreplyinput]')) {
|
|
4725
4750
|
inp.onclick = (e) => e.stopPropagation();
|
|
4726
|
-
inp.onkeydown = (e) => { if (e.key === 'Enter') { e.preventDefault(); sendQReply(inp.dataset.qreplyinput, inp); } };
|
|
4751
|
+
inp.onkeydown = (e) => { if (e.key === 'Enter' && !e.isComposing) { e.preventDefault(); sendQReply(inp.dataset.qreplyinput, inp); } };
|
|
4727
4752
|
}
|
|
4728
4753
|
// Pending rows open the same detail dialog as Review (tap anywhere but the
|
|
4729
4754
|
// Start/Delete buttons, which stop propagation in their own handlers).
|
|
@@ -4847,7 +4872,7 @@ function startGoalheadEdit(id, btn){
|
|
|
4847
4872
|
const save = async () => { const t = inp.value.trim(); if (!t) { wrap.remove(); return; }
|
|
4848
4873
|
const r = await fetch(withKey(`/api/goals/${id}`), { method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ text: t }) });
|
|
4849
4874
|
if (!r.ok) showErr('Cannot edit a running/finished goal — reply to append instead'); await refresh(); };
|
|
4850
|
-
inp.onkeydown = (e) => { if (e.key === 'Enter') { e.preventDefault(); save(); } if (e.key === 'Escape') wrap.remove(); };
|
|
4875
|
+
inp.onkeydown = (e) => { if (e.key === 'Enter' && !e.isComposing) { e.preventDefault(); save(); } if (e.key === 'Escape') wrap.remove(); };
|
|
4851
4876
|
}
|
|
4852
4877
|
// SL1 inline Reply: append an instruction to a started goal → POST /api/goals/:id/reply.
|
|
4853
4878
|
function openGoalheadReply(id, btn){
|
|
@@ -4860,7 +4885,7 @@ function openGoalheadReply(id, btn){
|
|
|
4860
4885
|
const send = async () => { const t = inp.value.trim(); if (!t) return;
|
|
4861
4886
|
const r = await fetch(withKey(`/api/goals/${id}/reply`), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ text: t }) });
|
|
4862
4887
|
if (!r.ok) showErr('未開始のゴールは編集を使ってください'); await refresh(); };
|
|
4863
|
-
inp.onkeydown = (e) => { if (e.key === 'Enter') { e.preventDefault(); send(); } if (e.key === 'Escape') wrap.remove(); };
|
|
4888
|
+
inp.onkeydown = (e) => { if (e.key === 'Enter' && !e.isComposing) { e.preventDefault(); send(); } if (e.key === 'Escape') wrap.remove(); };
|
|
4864
4889
|
snd.onclick = send;
|
|
4865
4890
|
}
|
|
4866
4891
|
|
|
@@ -5180,6 +5205,18 @@ function renderReviewChecklist(){
|
|
|
5180
5205
|
$('reviewApprove').style.display = 'none'; // Feedback: drop the duplicate goal-level Approve — per-requirement Approve is enough
|
|
5181
5206
|
$('reviewDismiss').style.display = canReview ? '' : 'none';
|
|
5182
5207
|
$('reviewApprove').disabled = !canReview;
|
|
5208
|
+
// A folded goal opens here read-only (Masa 2026-07-23, [Open] from "Also carries"): nothing
|
|
5209
|
+
// to approve, so don't show the approve-each-requirement intro — say honestly where its
|
|
5210
|
+
// work went. Set it in BOTH branches (this overlay is reused across goals, so a stale
|
|
5211
|
+
// folded-intro must not linger on the next review goal).
|
|
5212
|
+
const introEl = $('reviewIntro');
|
|
5213
|
+
if (introEl) {
|
|
5214
|
+
introEl.textContent = canReview
|
|
5215
|
+
? 'Review each requirement with its capture and approve it individually — approved ones fold away. Or approve the whole goal below to send it straight to Done; the next review pops in automatically. Tap outside to close.'
|
|
5216
|
+
: goal.status === 'folded'
|
|
5217
|
+
? "This request's work was merged into another review's PR, so there is nothing to approve here — its work is shown below. Undo the merge from “Also carries” to restore it as its own review."
|
|
5218
|
+
: 'Review the details below. Tap outside to close.';
|
|
5219
|
+
}
|
|
5183
5220
|
renderPrevToast(goalId);
|
|
5184
5221
|
// task 96: 件数だけでなく「このゴールで何をやったか・何件完了か」を要約表示。
|
|
5185
5222
|
const sum = buildReviewSummary({ goal, tasks: reqs, approvedN });
|
|
@@ -5741,7 +5778,7 @@ async function sendReviewReject(){
|
|
|
5741
5778
|
$('reviewDismiss').onclick = () => sendReviewReject();
|
|
5742
5779
|
$('reviewFixSend').onclick = () => { const t = $('reviewFixInput').value.trim(); if (t || reviewAttach.length) sendReviewDismiss(t); };
|
|
5743
5780
|
$('reviewFixInput').addEventListener('keydown', (e) => {
|
|
5744
|
-
if (e.key === 'Enter') { const t = $('reviewFixInput').value.trim(); if (t || reviewAttach.length) sendReviewDismiss(t); }
|
|
5781
|
+
if (e.key === 'Enter' && !e.isComposing) { const t = $('reviewFixInput').value.trim(); if (t || reviewAttach.length) sendReviewDismiss(t); }
|
|
5745
5782
|
});
|
|
5746
5783
|
$('reviewFixImg').onclick = () => $('reviewFixFile').click();
|
|
5747
5784
|
$('reviewFixFile').addEventListener('change', async () => {
|
|
@@ -6547,9 +6584,11 @@ function renderFsReview(){
|
|
|
6547
6584
|
const pgoals = state.goals.filter((g) => g.projectId === state.active);
|
|
6548
6585
|
const list = state.tasks.filter((t) => t.projectId === state.active);
|
|
6549
6586
|
const rows = buildReviewDigest({ goals: pgoals, tasks: list });
|
|
6550
|
-
// PRD §
|
|
6551
|
-
// Review
|
|
6552
|
-
// went
|
|
6587
|
+
// PRD §6.5 → Masa 2026-07-23: a goal folded onto another review's PR does NOT get its own
|
|
6588
|
+
// row in the Review list (Masa, seeing it live: "ここにはいらない・逆にわかりにくい"). Where
|
|
6589
|
+
// its work went — and the control to restore it — live on the carrier's side only, in the
|
|
6590
|
+
// opened detail's "Also carries" section (see saItemHtml's carriesBody). The list stays
|
|
6591
|
+
// just the reviews; folded goals are explained where they were merged.
|
|
6553
6592
|
if (!rows.length) { el.innerHTML = ''; return; } // §1.5-3: no reviews → the UI itself does not exist
|
|
6554
6593
|
const green = rows.filter((r) => !r.testResult || r.testResult.ok !== false).length;
|
|
6555
6594
|
const blockedN = pgoals.filter((g) => g.status === 'blocked').length;
|
|
@@ -6626,8 +6665,8 @@ function renderFsReview(){
|
|
|
6626
6665
|
if (e.target.closest('a') || e.target.closest('.act')) return;
|
|
6627
6666
|
openReviewChecklist(Number(c.dataset.fsrev));
|
|
6628
6667
|
};
|
|
6629
|
-
// Folded goals no
|
|
6630
|
-
//
|
|
6668
|
+
// Folded goals get no row here (Masa 2026-07-23) — they are shown, and restored, on the
|
|
6669
|
+
// carrier's side in the See-all "Also carries" section (saItemHtml + the seeall handler).
|
|
6631
6670
|
for (const b of el.querySelectorAll('[data-fsapprove]')) b.onclick = async (e) => { e.stopPropagation();
|
|
6632
6671
|
let ok = true, mergeGoal = null;
|
|
6633
6672
|
for (const id of b.dataset.fsapprove.split(',')) {
|
|
@@ -6895,7 +6934,7 @@ function inlineFsRename(span, onCommit){
|
|
|
6895
6934
|
if (changed) onCommit?.(v);
|
|
6896
6935
|
span.removeEventListener('keydown', onKey); span.removeEventListener('keyup', onKeyUp); span.removeEventListener('blur', onBlur); };
|
|
6897
6936
|
const onKey = (ev) => { ev.stopPropagation();
|
|
6898
|
-
if (ev.key === 'Enter') { ev.preventDefault(); finish(true); span.blur(); }
|
|
6937
|
+
if (ev.key === 'Enter' && !ev.isComposing) { ev.preventDefault(); finish(true); span.blur(); }
|
|
6899
6938
|
else if (ev.key === 'Escape') { ev.preventDefault(); finish(false); span.blur(); } };
|
|
6900
6939
|
const onKeyUp = (ev) => { ev.stopPropagation(); };
|
|
6901
6940
|
const onBlur = () => finish(true);
|
|
@@ -8644,6 +8683,13 @@ const MSG_VIA = { composer: 'chat', review: 'review', thread: 'note' };
|
|
|
8644
8683
|
function saMsgRowHtml(m, prevVia){
|
|
8645
8684
|
const who = MSG_WHO[m?.from] ?? 'AI';
|
|
8646
8685
|
const cls = m?.from === 'you' ? 'you' : m?.from === 'manager' ? 'mgr' : 'cl';
|
|
8686
|
+
// A folded sibling's work was merged onto this goal (engine stamps from:manager/via:fold,
|
|
8687
|
+
// Masa 2026-07-23). Label it "merged in" so its text does not read as the Manager typing
|
|
8688
|
+
// a fresh request — the old you/via:composer stamp looked like a phantom re-submission.
|
|
8689
|
+
// Which card it came from is listed on the carrier's "Also carries" section.
|
|
8690
|
+
if (m?.via === 'fold') {
|
|
8691
|
+
return `<div class="txr mgr"><span class="w">Manager</span><span class="m">${esc(String(m?.text ?? ''))}</span><span class="cvia">merged in</span></div>`;
|
|
8692
|
+
}
|
|
8647
8693
|
// Manager rows carry no entrance: `who` already says this line is automatic.
|
|
8648
8694
|
const via = m?.from === 'manager' ? '' : (MSG_VIA[m?.via] ?? '');
|
|
8649
8695
|
const show = via && via !== prevVia;
|
|
@@ -8824,12 +8870,14 @@ function saItemHtml(it, i){
|
|
|
8824
8870
|
const openBody = (it.run?.cmd || it.run?.url)
|
|
8825
8871
|
? `<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>`
|
|
8826
8872
|
: '';
|
|
8827
|
-
// ── Also carries (Masa 2026-07-21): goals folded onto this review are
|
|
8828
|
-
// the review's own content —
|
|
8829
|
-
//
|
|
8830
|
-
//
|
|
8873
|
+
// ── Also carries (Masa 2026-07-21 → refined 2026-07-23): goals folded onto this review are
|
|
8874
|
+
// logged HERE, inside the review's own content — NOT as loose rows in the Review column
|
|
8875
|
+
// (Masa: "ここにはいらない・逆にわかりにくい"). This is the ONE place a folded goal is shown,
|
|
8876
|
+
// and where it is restored: each item gets [Open] (the Review dialog) and [Undo] (POST /unfold
|
|
8877
|
+
// — pull it back out as its own review + PR). Chrome label stays English (§4); the request
|
|
8878
|
+
// titles keep their language. Only rendered when something was folded in (§1.5-3 no empty box). ──
|
|
8831
8879
|
const carriesBody = it.folded?.length
|
|
8832
|
-
? `<ul class="rcheck">${it.folded.map((f) => `<li>#${esc(String(f.id))} ${esc(f.title)}</li>`).join('')}</ul>`
|
|
8880
|
+
? `<ul class="rcheck rcarries">${it.folded.map((f) => `<li><span class="cft">#${esc(String(f.id))} ${esc(f.title)}</span><span class="cfa"><button class="cfb" data-saact="foldopen:${f.id}">Open</button><button class="cfb" data-saact="foldundo:${f.id}" title="Undo the merge — restore this as its own review">Undo</button></span></li>`).join('')}</ul>`
|
|
8833
8881
|
: '';
|
|
8834
8882
|
// ── What to check (spec §2.5): a bulleted list when several, a line when one. ──
|
|
8835
8883
|
// Only an explicit review-summary check renders here — never the goal text echoed back
|
|
@@ -9256,8 +9304,19 @@ $('seeall').addEventListener('click', async (e) => {
|
|
|
9256
9304
|
const b = e.target.closest('[data-saact]');
|
|
9257
9305
|
if (!b) return;
|
|
9258
9306
|
const [k, v] = b.dataset.saact.split(':'); const i = Number(v);
|
|
9259
|
-
const it = SA.items[i];
|
|
9260
9307
|
if (k === 'close') return saClose();
|
|
9308
|
+
// Also carries actions carry a GOAL id (not an item index): open the folded goal in the
|
|
9309
|
+
// Review dialog (Masa 2026-07-23 — same review-styled UI, not the plain goal detail; the
|
|
9310
|
+
// #reviewOverlay sits above #seeall, and a folded goal shows read-only there since
|
|
9311
|
+
// canReview=false hides Approve/Dismiss), or restore it via /unfold. Handled before the
|
|
9312
|
+
// SA.items[i] lookup below.
|
|
9313
|
+
if (k === 'foldopen') return openReviewChecklist(Number(v));
|
|
9314
|
+
if (k === 'foldundo') {
|
|
9315
|
+
const r = await fetch(withKey(`/api/goals/${v}/unfold`), { method: 'POST' });
|
|
9316
|
+
if (r && r.ok) await refresh(); else showErr('Undo failed.');
|
|
9317
|
+
return;
|
|
9318
|
+
}
|
|
9319
|
+
const it = SA.items[i];
|
|
9261
9320
|
if (!it) return;
|
|
9262
9321
|
// meta-row toggle (H23): tap the token chip to reveal the technique breakdown.
|
|
9263
9322
|
if (k === 'tok') { b.classList.toggle('open'); $('seeall').querySelector(`#tokdetail${i}`)?.classList.toggle('open'); return; }
|
|
@@ -9439,7 +9498,8 @@ function renderCtxBar(){
|
|
|
9439
9498
|
// LRM, so the rtl clipping trims the HEAD of the path without reversing the text
|
|
9440
9499
|
+ `<span class="pt">\u200e${esc(folderShort(r.dir))}</span></button>`)
|
|
9441
9500
|
.join('');
|
|
9442
|
-
|
|
9501
|
+
const openRow = `<button type="button" class="appopt ctxopen" id="ctxopenfolder" title="Open the OS folder chooser"><svg viewBox="0 0 24 24"><path d="M3 7a2 2 0 012-2h4l2 2h8a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z"/><path d="M12 11v6M9 14h6"/></svg><span class="nm">Open folder…</span></button>`;
|
|
9502
|
+
menu.innerHTML = `${opts}${openRow}<div class="ctxpath"><input type="text" id="ctxdirin" placeholder="~/path/to/repo" aria-label="Folder path" spellcheck="false"><button type="button" class="appopt" id="ctxdiruse" style="width:auto">Use</button></div><div class="ctxmsg" id="ctxdirmsg"></div>`;
|
|
9443
9503
|
}
|
|
9444
9504
|
// The worktree checkbox (engine #268). ON — the default, and what every goal has always
|
|
9445
9505
|
// done — gives each goal its own copy of the repo, so a run cannot leave this folder
|
|
@@ -9485,6 +9545,26 @@ async function setProjectFolder(dir){
|
|
|
9485
9545
|
render();
|
|
9486
9546
|
} catch { say('Could not reach the engine.', true); }
|
|
9487
9547
|
}
|
|
9548
|
+
// "Open folder…" the Claude-Code way. On a LOCAL session the engine opens the
|
|
9549
|
+
// operator's OWN native OS folder chooser (POST /api/choose-folder — engine PR)
|
|
9550
|
+
// and we point this project at the picked path. A remote/relay session can't
|
|
9551
|
+
// (the dialog would open on the server, not the person's screen), so it falls
|
|
9552
|
+
// back to the detected repos + typed path already in this menu.
|
|
9553
|
+
function isLocalSession(){ return ['localhost', '127.0.0.1'].includes(location.hostname); }
|
|
9554
|
+
async function openFolderNative(){
|
|
9555
|
+
const msg = $('ctxdirmsg');
|
|
9556
|
+
const say = (t, bad) => { if (msg) { msg.textContent = t; msg.classList.toggle('bad', Boolean(bad)); } };
|
|
9557
|
+
if (!isLocalSession()) { $('ctxdirin')?.focus(); say('Remote session — pick a repo above or type a path.'); return; }
|
|
9558
|
+
say('Opening the folder chooser…');
|
|
9559
|
+
try {
|
|
9560
|
+
const r = await fetch(withKey('/api/choose-folder'), { method: 'POST' });
|
|
9561
|
+
const body = await r.json().catch(() => ({}));
|
|
9562
|
+
if (body.path) { setProjectFolder(body.path); return; } // reuse: PUT the project's dir, close, render
|
|
9563
|
+
if (body.cancelled) { say(''); return; }
|
|
9564
|
+
if (body.unsupported) { say('This engine can’t open a folder dialog — pick a repo above or type a path.'); return; }
|
|
9565
|
+
say(body.error || 'Could not open the folder chooser.', true);
|
|
9566
|
+
} catch { say('Could not reach the engine.', true); }
|
|
9567
|
+
}
|
|
9488
9568
|
$('ctxdirbtn').onclick = (e) => {
|
|
9489
9569
|
e.stopPropagation();
|
|
9490
9570
|
const menu = $('ctxdirmenu');
|
|
@@ -9499,13 +9579,14 @@ $('ctxwt').onclick = (e) => {
|
|
|
9499
9579
|
};
|
|
9500
9580
|
$('ctxdirmenu').addEventListener('click', (e) => {
|
|
9501
9581
|
e.stopPropagation();
|
|
9582
|
+
if (e.target.closest('#ctxopenfolder')) { openFolderNative(); return; }
|
|
9502
9583
|
const opt = e.target.closest('[data-ctxdir]');
|
|
9503
9584
|
if (opt) { setProjectFolder(opt.dataset.ctxdir); return; }
|
|
9504
9585
|
if (e.target.closest('#ctxdiruse')) setProjectFolder($('ctxdirin')?.value ?? '');
|
|
9505
9586
|
});
|
|
9506
9587
|
$('ctxdirmenu').addEventListener('keydown', (e) => {
|
|
9507
9588
|
e.stopPropagation();
|
|
9508
|
-
if (e.key === 'Enter' && e.target.id === 'ctxdirin') { e.preventDefault(); setProjectFolder(e.target.value); }
|
|
9589
|
+
if (e.key === 'Enter' && !e.isComposing && e.target.id === 'ctxdirin') { e.preventDefault(); setProjectFolder(e.target.value); }
|
|
9509
9590
|
});
|
|
9510
9591
|
$('appbtn').onclick = (e) => { e.stopPropagation(); $('appmenu').classList.toggle('show'); };
|
|
9511
9592
|
$('appmenu').addEventListener('click', (e) => {
|
package/engine/lib.mjs
CHANGED
|
@@ -778,7 +778,10 @@ export function refinePlanTasks(tasks, { goalText, maxTasks = 4 } = {}) {
|
|
|
778
778
|
// - /api/state には載せない。開いた時だけ取りに行く(実装ログ #234/#238 と同じ形)。
|
|
779
779
|
export const MESSAGE_TEXT_LIMIT = 8000;
|
|
780
780
|
export const MESSAGE_FROM = ['you', 'ai', 'manager'];
|
|
781
|
-
|
|
781
|
+
// 'fold' = the review gate merged a sibling goal's work onto this one; the app
|
|
782
|
+
// renders it as a merge notice ("🔀 統合"), not as a human utterance (Masa
|
|
783
|
+
// 2026-07-23 — the old you/composer stamp looked like a phantom re-submission).
|
|
784
|
+
export const MESSAGE_VIA = ['composer', 'review', 'thread', 'fold'];
|
|
782
785
|
|
|
783
786
|
// 1発言を正規化する。text が空なら null(空の発言は記録しない=無いものを
|
|
784
787
|
// 残さない)。from/via が未知なら記録側の都合で捨てず、既定に寄せる。
|
|
@@ -1155,6 +1158,49 @@ export function isInconclusiveTestRun({ ran = false, timedOut = false, passed =
|
|
|
1155
1158
|
return Boolean(ran) && Boolean(timedOut) && !realAssertion && passed === 0;
|
|
1156
1159
|
}
|
|
1157
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
|
+
|
|
1158
1204
|
// The verification-gate decision (Masa 2026-07-19: "検証/proof は Failed を作らない").
|
|
1159
1205
|
// PROOF IS ADVISORY: a UI goal whose screenshot could not be captured
|
|
1160
1206
|
// (proofMissing) or whose pixel heuristic could not auto-confirm the target
|
|
@@ -3751,6 +3797,31 @@ export function folderLabel(dir, homeDir = '') {
|
|
|
3751
3797
|
return d.length <= 60 ? d : '…' + d.slice(-59);
|
|
3752
3798
|
}
|
|
3753
3799
|
|
|
3800
|
+
// The osascript program that opens the operator's native macOS folder chooser
|
|
3801
|
+
// and prints the picked folder's POSIX path. Kept here (not inline in the route)
|
|
3802
|
+
// so the exact script — and its quote-escaping — is one testable thing. `choose
|
|
3803
|
+
// folder` is a Standard Additions command; `POSIX path of` turns the returned
|
|
3804
|
+
// alias into an absolute path string.
|
|
3805
|
+
export function chooseFolderScript(prompt = 'Choose a project folder') {
|
|
3806
|
+
const safe = String(prompt).replace(/["\\]/g, ' ');
|
|
3807
|
+
return `POSIX path of (choose folder with prompt "${safe}")`;
|
|
3808
|
+
}
|
|
3809
|
+
|
|
3810
|
+
// Turn the result of running `osascript -e chooseFolderScript()` into a small
|
|
3811
|
+
// verdict the /api/choose-folder route returns. macOS prints the POSIX path
|
|
3812
|
+
// (with a trailing slash) on stdout; pressing Cancel exits non-zero with
|
|
3813
|
+
// "User canceled." on stderr. Pure, so the route's contract is unit-tested
|
|
3814
|
+
// without ever opening a GUI dialog.
|
|
3815
|
+
// { path } → a folder was chosen (trailing slash stripped; '/' preserved)
|
|
3816
|
+
// { cancelled } → the user dismissed the dialog
|
|
3817
|
+
// { error } → osascript failed for some other reason
|
|
3818
|
+
export function parseChosenFolder({ code = 0, stdout = '', stderr = '' } = {}) {
|
|
3819
|
+
const out = String(stdout).trim();
|
|
3820
|
+
if (Number(code) === 0 && out) return { path: out.replace(/\/+$/, '') || '/' };
|
|
3821
|
+
if (/user cancell?ed/i.test(String(stderr))) return { cancelled: true };
|
|
3822
|
+
return { error: String(stderr).trim() || 'choose folder failed' };
|
|
3823
|
+
}
|
|
3824
|
+
|
|
3754
3825
|
// Build the "which folder should I work in?" clarifying question from detected
|
|
3755
3826
|
// repos. Reuses the existing needsInput card ({text, options:string[]}), and
|
|
3756
3827
|
// adds kind:'folder' + folders[] + lang so the /answer handler rebinds the
|
package/engine/server.mjs
CHANGED
|
@@ -20,8 +20,8 @@ import { resolve, dirname, join, basename } from 'node:path';
|
|
|
20
20
|
import { homedir } from 'node:os';
|
|
21
21
|
import { fileURLToPath } from 'node:url';
|
|
22
22
|
import { pathToFileURL } from 'node:url';
|
|
23
|
-
import { needsProjectFolder, folderLabel, 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';
|
|
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, 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
|
|
@@ -2839,7 +2850,12 @@ async function finishGoalIfComplete(goal, project, activityTask = null) {
|
|
|
2839
2850
|
// Same owner only: folding across owners would splice one user's work onto
|
|
2840
2851
|
// another's PR and echo its text back — the same leak the intake-side fold
|
|
2841
2852
|
// guards against with a visibility check.
|
|
2842
|
-
|
|
2853
|
+
// `noFold` = the human hit 「戻す」 on a folded card (POST /unfold): re-run
|
|
2854
|
+
// close-out but DON'T fold back into the same target it was just pulled out of
|
|
2855
|
+
// (that would be an instant loop, since the target still sits in review with
|
|
2856
|
+
// the overlapping files). The card then opens its own PR and both stay in
|
|
2857
|
+
// Review, flagged via conflictsWith — the visible collision Masa asked for.
|
|
2858
|
+
const foldTarget = (conflicts.length && !goal.noFold)
|
|
2843
2859
|
? goals.find((g) => g.id === pickFoldTarget(
|
|
2844
2860
|
{ id: goal.id, changedFiles: goalChangedFileUnion(goal.id) },
|
|
2845
2861
|
goals
|
|
@@ -2855,10 +2871,16 @@ async function finishGoalIfComplete(goal, project, activityTask = null) {
|
|
|
2855
2871
|
// The target re-opens: it has new work to do, and createGoalPR's rework path
|
|
2856
2872
|
// will push the result onto the PR branch it already owns.
|
|
2857
2873
|
foldTarget.status = 'running';
|
|
2858
|
-
|
|
2874
|
+
// from:'manager' via:'fold' (NOT you/composer): this is the engine merging a
|
|
2875
|
+
// sibling's work, not the human typing. The old you/composer stamp made the
|
|
2876
|
+
// folded goal's text look like a phantom re-submission by the user in the
|
|
2877
|
+
// target's thread (Masa 2026-07-23). The app renders via:'fold' as a merge
|
|
2878
|
+
// notice; the folded card itself carries the "→ #N 統合済み" pointer.
|
|
2879
|
+
queueReplyTask(foldTarget, goal.text.slice(0, 8000), { from: 'manager', via: 'fold' });
|
|
2859
2880
|
saveGoal(foldTarget);
|
|
2860
2881
|
// The folded goal keeps its row (PRD §3.5: a submission is never dropped) —
|
|
2861
|
-
// it becomes a pointer at the goal that now carries its work.
|
|
2882
|
+
// it becomes a pointer at the goal that now carries its work. The app renders
|
|
2883
|
+
// it as a visible "→ #N 統合済み [開く][戻す]" card so it never vanishes.
|
|
2862
2884
|
goal.status = 'folded';
|
|
2863
2885
|
goal.foldedInto = foldTarget.id;
|
|
2864
2886
|
saveGoal(goal);
|
|
@@ -2971,6 +2993,15 @@ async function verifyGate(goal, prProject, siblings, baseProject, activityTask =
|
|
|
2971
2993
|
goal.testResult.inconclusive = true;
|
|
2972
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.');
|
|
2973
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
|
+
}
|
|
2974
3005
|
// Baseline diff (Masa 2026-07-19): block ONLY on failures THIS change
|
|
2975
3006
|
// introduced. A test already red on the base — e.g. persist-failure on clean
|
|
2976
3007
|
// origin/main — is pre-existing noise and must not knock a proof-verified
|
|
@@ -3435,6 +3466,27 @@ const server = createServer(async (req, res) => {
|
|
|
3435
3466
|
return json(res, 200, { repos: detectRepos(HOME), home: HOME });
|
|
3436
3467
|
}
|
|
3437
3468
|
|
|
3469
|
+
// Open the operator's OWN native OS folder chooser and hand back the absolute
|
|
3470
|
+
// path — the honest "Open folder…" for a LOCAL session, matching Claude Code.
|
|
3471
|
+
// The dialog opens on the machine running the engine, so the browser only calls
|
|
3472
|
+
// this when it is local (same host); a remote/relay UI uses /api/repos instead
|
|
3473
|
+
// (otherwise the dialog would open on the server, not the person's screen). It
|
|
3474
|
+
// returns only the chosen path — nothing is scanned, listed, or invented.
|
|
3475
|
+
// { path, label } | { cancelled: true } | { unsupported: true } | { error }
|
|
3476
|
+
// MANAGER_NO_NATIVE_DIALOG (and any non-macOS host) short-circuits to
|
|
3477
|
+
// {unsupported} so tests/CI/headless never block on a GUI.
|
|
3478
|
+
if (url.pathname === '/api/choose-folder' && req.method === 'POST') {
|
|
3479
|
+
if (process.platform !== 'darwin' || process.env.MANAGER_NO_NATIVE_DIALOG) {
|
|
3480
|
+
return json(res, 200, { unsupported: true });
|
|
3481
|
+
}
|
|
3482
|
+
execFile('osascript', ['-e', chooseFolderScript()], { timeout: 120000 }, (err, stdout, stderr) => {
|
|
3483
|
+
const verdict = parseChosenFolder({ code: err ? (err.code ?? 1) : 0, stdout, stderr });
|
|
3484
|
+
if (verdict.path) verdict.label = folderLabel(verdict.path, HOME);
|
|
3485
|
+
json(res, 200, verdict);
|
|
3486
|
+
});
|
|
3487
|
+
return;
|
|
3488
|
+
}
|
|
3489
|
+
|
|
3438
3490
|
// The project's working rules (ONE-CONVERSATION §1.5a): the settings panel
|
|
3439
3491
|
// shows a read-only view of whichever of CLAUDE.md / AGENTS.md exist in the
|
|
3440
3492
|
// project dir — the instructions the connected AI actually reads. The browser
|
|
@@ -4011,6 +4063,31 @@ const server = createServer(async (req, res) => {
|
|
|
4011
4063
|
return json(res, 200, goal);
|
|
4012
4064
|
}
|
|
4013
4065
|
|
|
4066
|
+
// 「戻す」on a folded card (Masa 2026-07-23 — 畳むけど見える・戻せる): pull a
|
|
4067
|
+
// folded goal back out of the target it was merged into and give it its own
|
|
4068
|
+
// PR. We flag noFold so finishGoalIfComplete's review gate does NOT fold it
|
|
4069
|
+
// straight back into the same target (which still sits in review with the
|
|
4070
|
+
// overlapping files); the re-run opens a separate PR and both cards stay in
|
|
4071
|
+
// Review, flagged via conflictsWith. Non-destructive to the target: whatever
|
|
4072
|
+
// work already merged into it stays — this only restores the folded card.
|
|
4073
|
+
const unfoldMatch = url.pathname.match(/^\/api\/goals\/(\d+)\/unfold$/);
|
|
4074
|
+
if (unfoldMatch && req.method === 'POST') {
|
|
4075
|
+
const goal = goals.find((g) => g.id === Number(unfoldMatch[1]));
|
|
4076
|
+
if (!goal) return json(res, 404, { error: 'goal not found' });
|
|
4077
|
+
if (!requireGoalOwnership(req, res, goal)) return;
|
|
4078
|
+
if (goal.status !== 'folded') return json(res, 409, { error: 'goal is not folded' });
|
|
4079
|
+
const project = projects.find((p) => p.id === goal.projectId);
|
|
4080
|
+
if (!project) return json(res, 404, { error: 'project not found' });
|
|
4081
|
+
goal.foldedInto = null;
|
|
4082
|
+
goal.noFold = true;
|
|
4083
|
+
goal.status = 'running'; // let finishGoalIfComplete re-run close-out (its tasks are all done → opens its own PR)
|
|
4084
|
+
saveGoal(goal);
|
|
4085
|
+
finishGoalIfComplete(goal, project).catch((e) => {
|
|
4086
|
+
goal.status = 'failed'; goal.prError = String(e).slice(0, 300); saveGoal(goal);
|
|
4087
|
+
});
|
|
4088
|
+
return json(res, 200, goal);
|
|
4089
|
+
}
|
|
4090
|
+
|
|
4014
4091
|
// Pending (deliberately shelved) goals: activate moves one into the normal
|
|
4015
4092
|
// breakdown-first flow; pend shelves a stacked goal.
|
|
4016
4093
|
const activateMatch = url.pathname.match(/^\/api\/goals\/(\d+)\/activate$/);
|
package/package.json
CHANGED