@galda/cli 0.10.41 → 0.10.43
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 +161 -32
- package/engine/lib.mjs +71 -1
- package/engine/server.mjs +106 -8
- 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)}
|
|
@@ -1218,6 +1236,14 @@
|
|
|
1218
1236
|
.gearpop .srow.dis{opacity:.38;pointer-events:none}
|
|
1219
1237
|
.gearpop .srow .sl{flex:1;min-width:0;font-size:12.5px;color:var(--ink)}
|
|
1220
1238
|
.gearpop .srow .sd{display:block;font-size:10.5px;color:var(--ink3);margin-top:2px}
|
|
1239
|
+
/* Project rules read-only view (§1.5a): a quiet framed panel under its row —
|
|
1240
|
+
mono filename label + the file body, scrollable. Reuses --canvas/--hair so it
|
|
1241
|
+
matches the MCP box; no new surface invented. */
|
|
1242
|
+
.gearpop .rulesview{margin:2px 0 9px;display:flex;flex-direction:column;gap:8px}
|
|
1243
|
+
.gearpop .rulesfile{background:var(--canvas);border-radius:8px;overflow:hidden}
|
|
1244
|
+
.gearpop .rulesname{font:600 9px/1 var(--mono);letter-spacing:.08em;text-transform:uppercase;color:var(--ink3);padding:8px 10px 0}
|
|
1245
|
+
.gearpop .rulestext{margin:6px 0 0;padding:0 10px 10px;max-height:220px;overflow:auto;font:400 11px/1.6 var(--mono);color:var(--ink2);white-space:pre-wrap;word-break:break-word}
|
|
1246
|
+
.gearpop .rulesempty{color:var(--ink3);font-style:italic}
|
|
1221
1247
|
/* MCP tab: live status dot + connect command + quiet explanation */
|
|
1222
1248
|
.gearpop .mcpstat{display:flex;align-items:center;gap:8px;font:600 11px/1 var(--ui);color:var(--ink);padding:2px 0 10px}
|
|
1223
1249
|
.gearpop .mcpstat i{width:7px;height:7px;border-radius:50%;background:var(--green);box-shadow:0 0 6px rgba(var(--green-rgb),.5)}
|
|
@@ -2168,6 +2194,13 @@
|
|
|
2168
2194
|
.redesign .rask,.redesign .rcheck,.redesign .rcheck li,.redesign .rcheck1{color:var(--ink) !important}
|
|
2169
2195
|
.redesign ul.rcheck{margin:2px 0 0;padding:0 0 0 18px;font-size:14px;line-height:1.7;letter-spacing:-.012em}
|
|
2170
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)}
|
|
2171
2204
|
/* Activity (spec §3) = ONE Linear-docs pulldown. Heading plain; chevron on the FIRST
|
|
2172
2205
|
line; tapping opens the rest below, offset-indented (no left rule); Discussion folds
|
|
2173
2206
|
in at the bottom of the opened body. */
|
|
@@ -3110,7 +3143,7 @@
|
|
|
3110
3143
|
<div class="modal reviewmodal">
|
|
3111
3144
|
<div class="revcount" id="reviewCount">REVIEW</div>
|
|
3112
3145
|
<h3 id="reviewGoalTitle">Review</h3>
|
|
3113
|
-
<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>
|
|
3114
3147
|
<div class="rvtoast" id="reviewPrevToast" style="display:none"></div>
|
|
3115
3148
|
<div class="revsummary" id="reviewSummary">0 / 0 approved</div>
|
|
3116
3149
|
<div class="revdone" id="reviewDoneMsg">
|
|
@@ -3276,7 +3309,7 @@ const savedTheme = (() => {
|
|
|
3276
3309
|
if (v !== s) localStorage.setItem('theme', v);
|
|
3277
3310
|
return v;
|
|
3278
3311
|
})();
|
|
3279
|
-
const state = { repos: null, repoHome: '', /* GET /api/repos — the folders a project can point at (Context Bar) */ projects: [], goals: [], tasks: [], act: {}, actAt: {}, todos: {}, chatTurns: {}, active: 'default', editing: null, attach: [], connectCommand: '', models: ['sonnet'], agentModels: null, agentEfforts: null, agents: ['claude-code', 'codex'], expanded: new Set(), openThreads: new Set(), queueOrder: {}, externalActivity: [], selectedGoal: null, workflowColumns: {}, wfDraft: null, reviewDefinitions: {}, rdDraft: null, connectedApp: localStorage.getItem('sel:agent') || 'claude-code', reviewOpenGoal: null, reviewApprovals: {}, goalDetailOpen: null, qreplyOpen: new Set(), logCollapsed: localStorage.getItem('logCollapsed') !== '0', logEntries: [], layout: localStorage.getItem('layout') || 'flagship', theme: savedTheme, boardskin: localStorage.getItem('boardskin') || 'auto', summaryPattern: Number(localStorage.getItem('summaryPattern')) || 3, peraSpot: 0, skill: null, skillsCache: null,
|
|
3312
|
+
const state = { repos: null, repoHome: '', /* GET /api/repos — the folders a project can point at (Context Bar) */ projects: [], goals: [], tasks: [], act: {}, actAt: {}, todos: {}, chatTurns: {}, active: 'default', editing: null, attach: [], connectCommand: '', models: ['sonnet'], agentModels: null, agentEfforts: null, agents: ['claude-code', 'codex'], expanded: new Set(), openThreads: new Set(), queueOrder: {}, externalActivity: [], selectedGoal: null, workflowColumns: {}, wfDraft: null, reviewDefinitions: {}, rdDraft: null, connectedApp: localStorage.getItem('sel:agent') || 'claude-code', reviewOpenGoal: null, reviewApprovals: {}, goalDetailOpen: null, qreplyOpen: new Set(), logCollapsed: localStorage.getItem('logCollapsed') !== '0', logEntries: [], layout: localStorage.getItem('layout') || 'flagship', theme: savedTheme, boardskin: localStorage.getItem('boardskin') || 'auto', summaryPattern: Number(localStorage.getItem('summaryPattern')) || 3, peraSpot: 0, skill: null, skillsCache: null, projectRules: {}, rulesOpen: false,
|
|
3280
3313
|
// per-message send overrides set via slash-commands (reset after each send,
|
|
3281
3314
|
// except model which is sticky in localStorage). palette = open command list.
|
|
3282
3315
|
msg: { mode: null, model: null, effort: null, skill: null, later: false, review: false },
|
|
@@ -5172,6 +5205,18 @@ function renderReviewChecklist(){
|
|
|
5172
5205
|
$('reviewApprove').style.display = 'none'; // Feedback: drop the duplicate goal-level Approve — per-requirement Approve is enough
|
|
5173
5206
|
$('reviewDismiss').style.display = canReview ? '' : 'none';
|
|
5174
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
|
+
}
|
|
5175
5220
|
renderPrevToast(goalId);
|
|
5176
5221
|
// task 96: 件数だけでなく「このゴールで何をやったか・何件完了か」を要約表示。
|
|
5177
5222
|
const sum = buildReviewSummary({ goal, tasks: reqs, approvedN });
|
|
@@ -6539,9 +6584,11 @@ function renderFsReview(){
|
|
|
6539
6584
|
const pgoals = state.goals.filter((g) => g.projectId === state.active);
|
|
6540
6585
|
const list = state.tasks.filter((t) => t.projectId === state.active);
|
|
6541
6586
|
const rows = buildReviewDigest({ goals: pgoals, tasks: list });
|
|
6542
|
-
// PRD §
|
|
6543
|
-
// Review
|
|
6544
|
-
// 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.
|
|
6545
6592
|
if (!rows.length) { el.innerHTML = ''; return; } // §1.5-3: no reviews → the UI itself does not exist
|
|
6546
6593
|
const green = rows.filter((r) => !r.testResult || r.testResult.ok !== false).length;
|
|
6547
6594
|
const blockedN = pgoals.filter((g) => g.status === 'blocked').length;
|
|
@@ -6618,8 +6665,8 @@ function renderFsReview(){
|
|
|
6618
6665
|
if (e.target.closest('a') || e.target.closest('.act')) return;
|
|
6619
6666
|
openReviewChecklist(Number(c.dataset.fsrev));
|
|
6620
6667
|
};
|
|
6621
|
-
// Folded goals no
|
|
6622
|
-
//
|
|
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).
|
|
6623
6670
|
for (const b of el.querySelectorAll('[data-fsapprove]')) b.onclick = async (e) => { e.stopPropagation();
|
|
6624
6671
|
let ok = true, mergeGoal = null;
|
|
6625
6672
|
for (const id of b.dataset.fsapprove.split(',')) {
|
|
@@ -8636,6 +8683,13 @@ const MSG_VIA = { composer: 'chat', review: 'review', thread: 'note' };
|
|
|
8636
8683
|
function saMsgRowHtml(m, prevVia){
|
|
8637
8684
|
const who = MSG_WHO[m?.from] ?? 'AI';
|
|
8638
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
|
+
}
|
|
8639
8693
|
// Manager rows carry no entrance: `who` already says this line is automatic.
|
|
8640
8694
|
const via = m?.from === 'manager' ? '' : (MSG_VIA[m?.via] ?? '');
|
|
8641
8695
|
const show = via && via !== prevVia;
|
|
@@ -8816,12 +8870,14 @@ function saItemHtml(it, i){
|
|
|
8816
8870
|
const openBody = (it.run?.cmd || it.run?.url)
|
|
8817
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>`
|
|
8818
8872
|
: '';
|
|
8819
|
-
// ── Also carries (Masa 2026-07-21): goals folded onto this review are
|
|
8820
|
-
// the review's own content —
|
|
8821
|
-
//
|
|
8822
|
-
//
|
|
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). ──
|
|
8823
8879
|
const carriesBody = it.folded?.length
|
|
8824
|
-
? `<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>`
|
|
8825
8881
|
: '';
|
|
8826
8882
|
// ── What to check (spec §2.5): a bulleted list when several, a line when one. ──
|
|
8827
8883
|
// Only an explicit review-summary check renders here — never the goal text echoed back
|
|
@@ -9248,8 +9304,19 @@ $('seeall').addEventListener('click', async (e) => {
|
|
|
9248
9304
|
const b = e.target.closest('[data-saact]');
|
|
9249
9305
|
if (!b) return;
|
|
9250
9306
|
const [k, v] = b.dataset.saact.split(':'); const i = Number(v);
|
|
9251
|
-
const it = SA.items[i];
|
|
9252
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];
|
|
9253
9320
|
if (!it) return;
|
|
9254
9321
|
// meta-row toggle (H23): tap the token chip to reveal the technique breakdown.
|
|
9255
9322
|
if (k === 'tok') { b.classList.toggle('open'); $('seeall').querySelector(`#tokdetail${i}`)?.classList.toggle('open'); return; }
|
|
@@ -9431,7 +9498,8 @@ function renderCtxBar(){
|
|
|
9431
9498
|
// LRM, so the rtl clipping trims the HEAD of the path without reversing the text
|
|
9432
9499
|
+ `<span class="pt">\u200e${esc(folderShort(r.dir))}</span></button>`)
|
|
9433
9500
|
.join('');
|
|
9434
|
-
|
|
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>`;
|
|
9435
9503
|
}
|
|
9436
9504
|
// The worktree checkbox (engine #268). ON — the default, and what every goal has always
|
|
9437
9505
|
// done — gives each goal its own copy of the repo, so a run cannot leave this folder
|
|
@@ -9477,6 +9545,26 @@ async function setProjectFolder(dir){
|
|
|
9477
9545
|
render();
|
|
9478
9546
|
} catch { say('Could not reach the engine.', true); }
|
|
9479
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
|
+
}
|
|
9480
9568
|
$('ctxdirbtn').onclick = (e) => {
|
|
9481
9569
|
e.stopPropagation();
|
|
9482
9570
|
const menu = $('ctxdirmenu');
|
|
@@ -9491,6 +9579,7 @@ $('ctxwt').onclick = (e) => {
|
|
|
9491
9579
|
};
|
|
9492
9580
|
$('ctxdirmenu').addEventListener('click', (e) => {
|
|
9493
9581
|
e.stopPropagation();
|
|
9582
|
+
if (e.target.closest('#ctxopenfolder')) { openFolderNative(); return; }
|
|
9494
9583
|
const opt = e.target.closest('[data-ctxdir]');
|
|
9495
9584
|
if (opt) { setProjectFolder(opt.dataset.ctxdir); return; }
|
|
9496
9585
|
if (e.target.closest('#ctxdiruse')) setProjectFolder($('ctxdirin')?.value ?? '');
|
|
@@ -10526,8 +10615,44 @@ function gearSetHTML(){
|
|
|
10526
10615
|
<button class="optchip${lang === 'en' ? ' on' : ''}" data-gslang="en">EN</button>
|
|
10527
10616
|
<button class="optchip${lang === 'ja' ? ' on' : ''}" data-gslang="ja">JA</button></div>
|
|
10528
10617
|
${billingRow}
|
|
10618
|
+
${gearRulesHTML()}
|
|
10529
10619
|
<div class="srow dis"><div class="sl">Data folder<span class="sd">Runs are stored in the engine's chat-runs folder on the host</span></div></div>`;
|
|
10530
10620
|
}
|
|
10621
|
+
// ONE-CONVERSATION §1.5a: the project's working rules — the file(s) the connected
|
|
10622
|
+
// AI actually reads (Claude Code → CLAUDE.md, Codex → AGENTS.md). Read-only: Galda
|
|
10623
|
+
// does NOT own these rules, it only surfaces them (editing is done in an editor or
|
|
10624
|
+
// by asking the AI in chat). §3 段階開示 — when the project has no such file the row
|
|
10625
|
+
// is absent entirely (never an empty container; the "ask once" flow is §1.5b).
|
|
10626
|
+
function gearRulesHTML(){
|
|
10627
|
+
const r = state.projectRules[state.active];
|
|
10628
|
+
if (!r || !r.files || !r.files.length) return '';
|
|
10629
|
+
const names = r.files.map((f) => f.name).join(' · ');
|
|
10630
|
+
const view = state.rulesOpen
|
|
10631
|
+
? `<div class="rulesview">${r.files.map((f) =>
|
|
10632
|
+
`<div class="rulesfile"><div class="rulesname">${esc(f.name)}</div>`
|
|
10633
|
+
+ `<pre class="rulestext">${esc(f.content) || '<span class="rulesempty">(empty file)</span>'}</pre></div>`).join('')}</div>`
|
|
10634
|
+
: '';
|
|
10635
|
+
return `
|
|
10636
|
+
<div class="srow"><div class="sl">Project rules<span class="sd">What this project tells the AI — read-only · ${esc(names)}</span></div>
|
|
10637
|
+
<button class="optchip${state.rulesOpen ? ' on' : ''}" data-grules="1">${state.rulesOpen ? 'Hide' : 'View'}</button></div>
|
|
10638
|
+
${view}`;
|
|
10639
|
+
}
|
|
10640
|
+
// Fetch the active project's rules files once (cached per project), then rebuild
|
|
10641
|
+
// the settings panel so the row appears. The browser can't read the host FS, so
|
|
10642
|
+
// this is the only way in — GET /api/projects/:id/rules (engine, §1.5a).
|
|
10643
|
+
let rulesFetching = null;
|
|
10644
|
+
async function loadProjectRules(){
|
|
10645
|
+
const pid = state.active;
|
|
10646
|
+
if (state.projectRules[pid] || rulesFetching === pid) return;
|
|
10647
|
+
rulesFetching = pid;
|
|
10648
|
+
try {
|
|
10649
|
+
const r = await fetch(withKey(`/api/projects/${pid}/rules`));
|
|
10650
|
+
state.projectRules[pid] = r.ok ? await r.json() : { files: [] };
|
|
10651
|
+
} catch { state.projectRules[pid] = { files: [] }; }
|
|
10652
|
+
finally { rulesFetching = null; }
|
|
10653
|
+
// Only rebuild if the settings tab is still the one on screen.
|
|
10654
|
+
if ($('gearPop') && custTab === 'settings') buildGearPop();
|
|
10655
|
+
}
|
|
10531
10656
|
// MCP (v46): connection status dot + connectCommand + COPY + quiet explanation.
|
|
10532
10657
|
// The dot is the app's own live engine link (SSE up/down) — the only real signal here.
|
|
10533
10658
|
function gearMcpHTML(){
|
|
@@ -10578,6 +10703,9 @@ function buildGearPop(){
|
|
|
10578
10703
|
try { await fetch(withKey('/api/signout'), { method: 'POST' }); recentlySignedOut = true; }
|
|
10579
10704
|
finally { await refresh(); if ($('gearPop')) buildGearPop(); }
|
|
10580
10705
|
};
|
|
10706
|
+
const rulesBtn = pop.querySelector('[data-grules]');
|
|
10707
|
+
if (rulesBtn) rulesBtn.onclick = () => { state.rulesOpen = !state.rulesOpen; buildGearPop(); };
|
|
10708
|
+
loadProjectRules();
|
|
10581
10709
|
return;
|
|
10582
10710
|
}
|
|
10583
10711
|
if (custTab === 'mcp') {
|
|
@@ -10711,6 +10839,7 @@ function custPlace(){ // flagship: top-anchored band beside the rail (v45 §2);
|
|
|
10711
10839
|
}
|
|
10712
10840
|
function custOpen(tab){
|
|
10713
10841
|
custTab = tab || 'theme'; // v46 deep-link (flagship __custOpen(tab)): menu entries land on their tab
|
|
10842
|
+
state.rulesOpen = false; // rules panel starts collapsed each time the gear opens
|
|
10714
10843
|
custSnap = { cust: { ...cust }, theme: state.theme, band: fsUI.band };
|
|
10715
10844
|
buildGearPop(); custPlace();
|
|
10716
10845
|
$('gearPop').classList.add('open');
|
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 が未知なら記録側の都合で捨てず、既定に寄せる。
|
|
@@ -1135,6 +1138,26 @@ export function classifyTestGate({ current = [], baseline = [] } = {}) {
|
|
|
1135
1138
|
return { newFailures, preExisting, blocked: newFailures.length > 0 };
|
|
1136
1139
|
}
|
|
1137
1140
|
|
|
1141
|
+
// A manager test-run that was KILLED before it could finish is inconclusive, not
|
|
1142
|
+
// a red. When the machine is saturated (prod + several worker goals all standing
|
|
1143
|
+
// up real servers + headless Chrome), `npm test` gets killed at its execFile
|
|
1144
|
+
// timeout BEFORE node --test can print its "ℹ pass N / ℹ fail N" summary — so the
|
|
1145
|
+
// parse sees 0 passed and the killed process is counted as a single phantom
|
|
1146
|
+
// failure. That is the machine being too busy to run the suite, not the goal's
|
|
1147
|
+
// code breaking, and blocking a finished goal (with a PR) on it strands good work
|
|
1148
|
+
// (dogfood #602/#604, 2026-07-23: passed:0, "1 failing", every listed failure a
|
|
1149
|
+
// 30s timeout, no summary line).
|
|
1150
|
+
//
|
|
1151
|
+
// The signal is `timedOut` (execFile killed the process — killed/SIGTERM/ETIMEDOUT),
|
|
1152
|
+
// NOT the mere absence of a summary: a small suite that printed `not ok 1` and
|
|
1153
|
+
// exited 1 on its own is a REAL failure and must still block. We also require
|
|
1154
|
+
// passed===0 (a killed run that had already logged passes is unusual, but if any
|
|
1155
|
+
// test passed the suite clearly ran) and fail CLOSED on a real AssertionError that
|
|
1156
|
+
// shares the killed run.
|
|
1157
|
+
export function isInconclusiveTestRun({ ran = false, timedOut = false, passed = 0, realAssertion = false } = {}) {
|
|
1158
|
+
return Boolean(ran) && Boolean(timedOut) && !realAssertion && passed === 0;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1138
1161
|
// The verification-gate decision (Masa 2026-07-19: "検証/proof は Failed を作らない").
|
|
1139
1162
|
// PROOF IS ADVISORY: a UI goal whose screenshot could not be captured
|
|
1140
1163
|
// (proofMissing) or whose pixel heuristic could not auto-confirm the target
|
|
@@ -1979,6 +2002,28 @@ export function hasGitChanges(porcelainStatus) {
|
|
|
1979
2002
|
return Boolean(String(porcelainStatus ?? '').trim());
|
|
1980
2003
|
}
|
|
1981
2004
|
|
|
2005
|
+
// ---- project rules files (ONE-CONVERSATION §1.5a) --------------------------
|
|
2006
|
+
// The files that hold a project's working rules — the instructions the connected
|
|
2007
|
+
// AI actually reads (Claude Code reads CLAUDE.md from cwd; Codex reads AGENTS.md).
|
|
2008
|
+
// Galda does NOT own these rules: it only surfaces whichever files exist,
|
|
2009
|
+
// read-only. Order is fixed so the settings row is stable, and both are shown
|
|
2010
|
+
// when both exist (never show one and let the other read as "missing").
|
|
2011
|
+
export const PROJECT_RULES_FILENAMES = ['CLAUDE.md', 'AGENTS.md'];
|
|
2012
|
+
|
|
2013
|
+
// Given a project dir and a reader (dir,name)->string|null, return the rules
|
|
2014
|
+
// files that exist, in PROJECT_RULES_FILENAMES order. A file the reader can't
|
|
2015
|
+
// return (absent → null, or throws) is omitted, so the UI shows only what is
|
|
2016
|
+
// really there rather than an empty rules pane.
|
|
2017
|
+
export function collectProjectRules(dir, readFile) {
|
|
2018
|
+
const files = [];
|
|
2019
|
+
for (const name of PROJECT_RULES_FILENAMES) {
|
|
2020
|
+
let content = null;
|
|
2021
|
+
try { content = readFile(dir, name); } catch { content = null; }
|
|
2022
|
+
if (typeof content === 'string') files.push({ name, content });
|
|
2023
|
+
}
|
|
2024
|
+
return files;
|
|
2025
|
+
}
|
|
2026
|
+
|
|
1982
2027
|
// ---- merge-time conflict detection -----------------------------------------
|
|
1983
2028
|
// Parallel goals never corrupt each other's WORKING files (each runs in its
|
|
1984
2029
|
// own worktree) — but two goals that edited the same repo path will still
|
|
@@ -3709,6 +3754,31 @@ export function folderLabel(dir, homeDir = '') {
|
|
|
3709
3754
|
return d.length <= 60 ? d : '…' + d.slice(-59);
|
|
3710
3755
|
}
|
|
3711
3756
|
|
|
3757
|
+
// The osascript program that opens the operator's native macOS folder chooser
|
|
3758
|
+
// and prints the picked folder's POSIX path. Kept here (not inline in the route)
|
|
3759
|
+
// so the exact script — and its quote-escaping — is one testable thing. `choose
|
|
3760
|
+
// folder` is a Standard Additions command; `POSIX path of` turns the returned
|
|
3761
|
+
// alias into an absolute path string.
|
|
3762
|
+
export function chooseFolderScript(prompt = 'Choose a project folder') {
|
|
3763
|
+
const safe = String(prompt).replace(/["\\]/g, ' ');
|
|
3764
|
+
return `POSIX path of (choose folder with prompt "${safe}")`;
|
|
3765
|
+
}
|
|
3766
|
+
|
|
3767
|
+
// Turn the result of running `osascript -e chooseFolderScript()` into a small
|
|
3768
|
+
// verdict the /api/choose-folder route returns. macOS prints the POSIX path
|
|
3769
|
+
// (with a trailing slash) on stdout; pressing Cancel exits non-zero with
|
|
3770
|
+
// "User canceled." on stderr. Pure, so the route's contract is unit-tested
|
|
3771
|
+
// without ever opening a GUI dialog.
|
|
3772
|
+
// { path } → a folder was chosen (trailing slash stripped; '/' preserved)
|
|
3773
|
+
// { cancelled } → the user dismissed the dialog
|
|
3774
|
+
// { error } → osascript failed for some other reason
|
|
3775
|
+
export function parseChosenFolder({ code = 0, stdout = '', stderr = '' } = {}) {
|
|
3776
|
+
const out = String(stdout).trim();
|
|
3777
|
+
if (Number(code) === 0 && out) return { path: out.replace(/\/+$/, '') || '/' };
|
|
3778
|
+
if (/user cancell?ed/i.test(String(stderr))) return { cancelled: true };
|
|
3779
|
+
return { error: String(stderr).trim() || 'choose folder failed' };
|
|
3780
|
+
}
|
|
3781
|
+
|
|
3712
3782
|
// Build the "which folder should I work in?" clarifying question from detected
|
|
3713
3783
|
// repos. Reuses the existing needsInput card ({text, options:string[]}), and
|
|
3714
3784
|
// adds kind:'folder' + folders[] + lang so the /answer handler rebinds the
|
package/engine/server.mjs
CHANGED
|
@@ -20,9 +20,10 @@ 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, 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, 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
|
+
import { collectProjectRules } from './lib.mjs';
|
|
26
27
|
import { openPR } from './pr.mjs';
|
|
27
28
|
import { runVerification, exerciseUi } from './verify.mjs';
|
|
28
29
|
|
|
@@ -2465,8 +2466,11 @@ const testRunQueue = createSerialQueue();
|
|
|
2465
2466
|
// project's own `npm test` in the goal's worktree and parse node --test's
|
|
2466
2467
|
// "ℹ pass N / ℹ fail N" (or TAP "# pass/# fail"). Returns {ran,passed,failed,ok}.
|
|
2467
2468
|
function runTestsOnce(dir) {
|
|
2469
|
+
// The timeout is env-overridable so tests can force the killed-run path fast;
|
|
2470
|
+
// production keeps the 4-minute default.
|
|
2471
|
+
const runTimeout = Number(process.env.MANAGER_TEST_TIMEOUT_MS) || 240000;
|
|
2468
2472
|
return testRunQueue.run(() => new Promise((res) => {
|
|
2469
|
-
execFile('npm', ['test'], { cwd: dir, timeout:
|
|
2473
|
+
execFile('npm', ['test'], { cwd: dir, timeout: runTimeout, env: { ...process.env }, maxBuffer: 8 * 1024 * 1024 }, (e, out = '', err = '') => {
|
|
2470
2474
|
const text = `${out}\n${err}`;
|
|
2471
2475
|
const n = (re) => { const m = text.match(re); return m ? Number(m[1]) : null; };
|
|
2472
2476
|
const passed = n(/ℹ pass (\d+)/) ?? n(/# pass (\d+)/) ?? n(/(\d+) passing/) ?? 0;
|
|
@@ -2480,9 +2484,15 @@ function runTestsOnce(dir) {
|
|
|
2480
2484
|
const timeoutFlakes = (text.match(/did not report ready in time/g) || []).length;
|
|
2481
2485
|
const realAssertion = /AssertionError|Expected values to be|expected .+ (?:to |but )/i.test(text);
|
|
2482
2486
|
const failed = realAssertion ? failedTotal : Math.max(0, failedTotal - timeoutFlakes);
|
|
2487
|
+
// Was the run KILLED at the execFile timeout (machine too saturated to
|
|
2488
|
+
// finish), rather than exiting on its own? execFile sets killed=true and
|
|
2489
|
+
// signal='SIGTERM' on timeout. That is the inconclusive signal — a normal
|
|
2490
|
+
// non-zero exit (a small suite that printed `not ok` and exited 1) is NOT
|
|
2491
|
+
// killed and must still be treated as a real result (isInconclusiveTestRun).
|
|
2492
|
+
const timedOut = Boolean(e && (e.killed || e.signal === 'SIGTERM' || e.code === 'ETIMEDOUT'));
|
|
2483
2493
|
// Capture the failing test NAMES too (not just the count) so the gate can
|
|
2484
2494
|
// diff them against the base's failures and block only on NEW ones.
|
|
2485
|
-
res({ passed, failed, detail: testOutputExcerpt(text), failingNames: parseFailingTestNames(text) });
|
|
2495
|
+
res({ passed, failed, timedOut, realAssertion, detail: testOutputExcerpt(text), failingNames: parseFailingTestNames(text) });
|
|
2486
2496
|
});
|
|
2487
2497
|
}));
|
|
2488
2498
|
}
|
|
@@ -2501,11 +2511,16 @@ async function runProjectTests(dir) {
|
|
|
2501
2511
|
r = {
|
|
2502
2512
|
passed: Math.max(r.passed, r2.passed),
|
|
2503
2513
|
failed: Math.min(r.failed, r2.failed),
|
|
2514
|
+
// Only an ALL-killed pair is inconclusive: if EITHER attempt exited on its
|
|
2515
|
+
// own (timedOut=false) the result is conclusive. realAssertion is OR-ed so
|
|
2516
|
+
// a real red in either run still fails closed.
|
|
2517
|
+
timedOut: Boolean(r.timedOut && r2.timedOut),
|
|
2518
|
+
realAssertion: r.realAssertion || r2.realAssertion,
|
|
2504
2519
|
detail: kept.detail || r.detail || r2.detail || '',
|
|
2505
2520
|
failingNames: kept.failingNames ?? r.failingNames ?? [],
|
|
2506
2521
|
};
|
|
2507
2522
|
}
|
|
2508
|
-
return { ran: true, passed: r.passed, failed: r.failed, ok: r.failed === 0, detail: r.detail ?? '', failingNames: r.failingNames ?? [] };
|
|
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 ?? [] };
|
|
2509
2524
|
}
|
|
2510
2525
|
|
|
2511
2526
|
// Failing tests on the BASE (pre-change) checkout — the set the review gate
|
|
@@ -2824,7 +2839,12 @@ async function finishGoalIfComplete(goal, project, activityTask = null) {
|
|
|
2824
2839
|
// Same owner only: folding across owners would splice one user's work onto
|
|
2825
2840
|
// another's PR and echo its text back — the same leak the intake-side fold
|
|
2826
2841
|
// guards against with a visibility check.
|
|
2827
|
-
|
|
2842
|
+
// `noFold` = the human hit 「戻す」 on a folded card (POST /unfold): re-run
|
|
2843
|
+
// close-out but DON'T fold back into the same target it was just pulled out of
|
|
2844
|
+
// (that would be an instant loop, since the target still sits in review with
|
|
2845
|
+
// the overlapping files). The card then opens its own PR and both stay in
|
|
2846
|
+
// Review, flagged via conflictsWith — the visible collision Masa asked for.
|
|
2847
|
+
const foldTarget = (conflicts.length && !goal.noFold)
|
|
2828
2848
|
? goals.find((g) => g.id === pickFoldTarget(
|
|
2829
2849
|
{ id: goal.id, changedFiles: goalChangedFileUnion(goal.id) },
|
|
2830
2850
|
goals
|
|
@@ -2840,10 +2860,16 @@ async function finishGoalIfComplete(goal, project, activityTask = null) {
|
|
|
2840
2860
|
// The target re-opens: it has new work to do, and createGoalPR's rework path
|
|
2841
2861
|
// will push the result onto the PR branch it already owns.
|
|
2842
2862
|
foldTarget.status = 'running';
|
|
2843
|
-
|
|
2863
|
+
// from:'manager' via:'fold' (NOT you/composer): this is the engine merging a
|
|
2864
|
+
// sibling's work, not the human typing. The old you/composer stamp made the
|
|
2865
|
+
// folded goal's text look like a phantom re-submission by the user in the
|
|
2866
|
+
// target's thread (Masa 2026-07-23). The app renders via:'fold' as a merge
|
|
2867
|
+
// notice; the folded card itself carries the "→ #N 統合済み" pointer.
|
|
2868
|
+
queueReplyTask(foldTarget, goal.text.slice(0, 8000), { from: 'manager', via: 'fold' });
|
|
2844
2869
|
saveGoal(foldTarget);
|
|
2845
2870
|
// The folded goal keeps its row (PRD §3.5: a submission is never dropped) —
|
|
2846
|
-
// it becomes a pointer at the goal that now carries its work.
|
|
2871
|
+
// it becomes a pointer at the goal that now carries its work. The app renders
|
|
2872
|
+
// it as a visible "→ #N 統合済み [開く][戻す]" card so it never vanishes.
|
|
2847
2873
|
goal.status = 'folded';
|
|
2848
2874
|
goal.foldedInto = foldTarget.id;
|
|
2849
2875
|
saveGoal(goal);
|
|
@@ -2946,6 +2972,16 @@ async function verifyGate(goal, prProject, siblings, baseProject, activityTask =
|
|
|
2946
2972
|
// Manager-shaped repos.
|
|
2947
2973
|
const proofFailing = siblings.some((t) => t.proof && t.proof.pass === false);
|
|
2948
2974
|
let testsFailing = goal.testResult.ran && goal.testResult.failed > 0;
|
|
2975
|
+
// Inconclusive run (2026-07-23): the suite was killed before it could finish
|
|
2976
|
+
// (0 passed, no summary, no real assertion) — the machine was too saturated to
|
|
2977
|
+
// run the tests, not the code failing. Blocking a finished goal (with a PR) on
|
|
2978
|
+
// that stranded #602/#604. Treat it like the infra-flake discount above: don't
|
|
2979
|
+
// block, note it, and let a human re-run when the machine is idle.
|
|
2980
|
+
if (testsFailing && isInconclusiveTestRun(goal.testResult)) {
|
|
2981
|
+
testsFailing = false;
|
|
2982
|
+
goal.testResult.inconclusive = true;
|
|
2983
|
+
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
|
+
}
|
|
2949
2985
|
// Baseline diff (Masa 2026-07-19): block ONLY on failures THIS change
|
|
2950
2986
|
// introduced. A test already red on the base — e.g. persist-failure on clean
|
|
2951
2987
|
// origin/main — is pre-existing noise and must not knock a proof-verified
|
|
@@ -3410,6 +3446,43 @@ const server = createServer(async (req, res) => {
|
|
|
3410
3446
|
return json(res, 200, { repos: detectRepos(HOME), home: HOME });
|
|
3411
3447
|
}
|
|
3412
3448
|
|
|
3449
|
+
// Open the operator's OWN native OS folder chooser and hand back the absolute
|
|
3450
|
+
// path — the honest "Open folder…" for a LOCAL session, matching Claude Code.
|
|
3451
|
+
// The dialog opens on the machine running the engine, so the browser only calls
|
|
3452
|
+
// this when it is local (same host); a remote/relay UI uses /api/repos instead
|
|
3453
|
+
// (otherwise the dialog would open on the server, not the person's screen). It
|
|
3454
|
+
// returns only the chosen path — nothing is scanned, listed, or invented.
|
|
3455
|
+
// { path, label } | { cancelled: true } | { unsupported: true } | { error }
|
|
3456
|
+
// MANAGER_NO_NATIVE_DIALOG (and any non-macOS host) short-circuits to
|
|
3457
|
+
// {unsupported} so tests/CI/headless never block on a GUI.
|
|
3458
|
+
if (url.pathname === '/api/choose-folder' && req.method === 'POST') {
|
|
3459
|
+
if (process.platform !== 'darwin' || process.env.MANAGER_NO_NATIVE_DIALOG) {
|
|
3460
|
+
return json(res, 200, { unsupported: true });
|
|
3461
|
+
}
|
|
3462
|
+
execFile('osascript', ['-e', chooseFolderScript()], { timeout: 120000 }, (err, stdout, stderr) => {
|
|
3463
|
+
const verdict = parseChosenFolder({ code: err ? (err.code ?? 1) : 0, stdout, stderr });
|
|
3464
|
+
if (verdict.path) verdict.label = folderLabel(verdict.path, HOME);
|
|
3465
|
+
json(res, 200, verdict);
|
|
3466
|
+
});
|
|
3467
|
+
return;
|
|
3468
|
+
}
|
|
3469
|
+
|
|
3470
|
+
// The project's working rules (ONE-CONVERSATION §1.5a): the settings panel
|
|
3471
|
+
// shows a read-only view of whichever of CLAUDE.md / AGENTS.md exist in the
|
|
3472
|
+
// project dir — the instructions the connected AI actually reads. The browser
|
|
3473
|
+
// can't touch the filesystem, so it reads them here. Filenames are fixed
|
|
3474
|
+
// (collectProjectRules), so `project.dir` is the only path input and there is
|
|
3475
|
+
// no traversal surface.
|
|
3476
|
+
const rulesMatch = url.pathname.match(/^\/api\/projects\/([\w-]+)\/rules$/);
|
|
3477
|
+
if (rulesMatch && req.method === 'GET') {
|
|
3478
|
+
const project = projects.find((p) => p.id === rulesMatch[1]);
|
|
3479
|
+
if (!project) return json(res, 404, { error: 'project not found' });
|
|
3480
|
+
const files = collectProjectRules(project.dir, (dir, name) => {
|
|
3481
|
+
try { return readFileSync(join(dir, name), 'utf8'); } catch { return null; }
|
|
3482
|
+
});
|
|
3483
|
+
return json(res, 200, { dir: project.dir, files });
|
|
3484
|
+
}
|
|
3485
|
+
|
|
3413
3486
|
if (url.pathname === '/api/projects' && req.method === 'POST') {
|
|
3414
3487
|
let body = '';
|
|
3415
3488
|
req.on('data', (d) => { body += d; });
|
|
@@ -3970,6 +4043,31 @@ const server = createServer(async (req, res) => {
|
|
|
3970
4043
|
return json(res, 200, goal);
|
|
3971
4044
|
}
|
|
3972
4045
|
|
|
4046
|
+
// 「戻す」on a folded card (Masa 2026-07-23 — 畳むけど見える・戻せる): pull a
|
|
4047
|
+
// folded goal back out of the target it was merged into and give it its own
|
|
4048
|
+
// PR. We flag noFold so finishGoalIfComplete's review gate does NOT fold it
|
|
4049
|
+
// straight back into the same target (which still sits in review with the
|
|
4050
|
+
// overlapping files); the re-run opens a separate PR and both cards stay in
|
|
4051
|
+
// Review, flagged via conflictsWith. Non-destructive to the target: whatever
|
|
4052
|
+
// work already merged into it stays — this only restores the folded card.
|
|
4053
|
+
const unfoldMatch = url.pathname.match(/^\/api\/goals\/(\d+)\/unfold$/);
|
|
4054
|
+
if (unfoldMatch && req.method === 'POST') {
|
|
4055
|
+
const goal = goals.find((g) => g.id === Number(unfoldMatch[1]));
|
|
4056
|
+
if (!goal) return json(res, 404, { error: 'goal not found' });
|
|
4057
|
+
if (!requireGoalOwnership(req, res, goal)) return;
|
|
4058
|
+
if (goal.status !== 'folded') return json(res, 409, { error: 'goal is not folded' });
|
|
4059
|
+
const project = projects.find((p) => p.id === goal.projectId);
|
|
4060
|
+
if (!project) return json(res, 404, { error: 'project not found' });
|
|
4061
|
+
goal.foldedInto = null;
|
|
4062
|
+
goal.noFold = true;
|
|
4063
|
+
goal.status = 'running'; // let finishGoalIfComplete re-run close-out (its tasks are all done → opens its own PR)
|
|
4064
|
+
saveGoal(goal);
|
|
4065
|
+
finishGoalIfComplete(goal, project).catch((e) => {
|
|
4066
|
+
goal.status = 'failed'; goal.prError = String(e).slice(0, 300); saveGoal(goal);
|
|
4067
|
+
});
|
|
4068
|
+
return json(res, 200, goal);
|
|
4069
|
+
}
|
|
4070
|
+
|
|
3973
4071
|
// Pending (deliberately shelved) goals: activate moves one into the normal
|
|
3974
4072
|
// breakdown-first flow; pend shelves a stacked goal.
|
|
3975
4073
|
const activateMatch = url.pathname.match(/^\/api\/goals\/(\d+)\/activate$/);
|
package/package.json
CHANGED