@galda/cli 0.10.112 → 0.10.114
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/CLAUDE.md +5 -0
- package/LICENSE.md +33 -0
- package/app/index.html +82 -53
- package/bin/manager-for-ai.mjs +44 -9
- package/docs/SECURITY.md +183 -0
- package/engine/relay-pref.mjs +57 -0
- package/engine/server.mjs +36 -0
- package/package.json +9 -7
package/CLAUDE.md
CHANGED
|
@@ -57,6 +57,11 @@
|
|
|
57
57
|
- **UI(app/index.html・app/theme.css 等)に触る前に、必ず `docs/design/DESIGN-RULES.md` を読むこと**。特に §1.5 レイアウトの憲法(10条)と §5.5 美学。条文に反する実装はせず、変えたい場合は理由を添えて仲田さんの決定を仰ぐ。
|
|
58
58
|
- app/ のUIファイル編集時はhook(tools/design_rules_hook.sh)が自動でリマインドする。リマインドが出たら読んだ上で作業すること。
|
|
59
59
|
- 実装時の詳細契約は `docs/design/CTO-HANDOFF.md`(CDO→CTO引き継ぎ書)に従う。**既存パーツで作れない新しい画面・コンポーネントが必要になったら、自作せず実装前にCDOへ依頼すること。**
|
|
60
|
+
- 🔴 **UIの文言は英語(Masa決定 2026-07-31)**。ボタン・ラベル・見出し・プレースホルダ・空状態・折りたたみの開閉ラベル・トースト/エラーなど**製品が書く文字は全て英語**。日本語のUI文字列を新しく足さない。
|
|
61
|
+
- 境界は**「誰の言葉か」**。製品が書くもの=英語。**ユーザーやworkerの発言をそのまま出している所(依頼文・返信・報告・質問の原文)は原文のまま**=翻訳も言語強制もしない。
|
|
62
|
+
- **コードのコメントは対象外**(日本語のままでよい)。
|
|
63
|
+
- 🟡 **例外=日英の出し分け(`failureLang`/`prefersJapaneseText`/`FAILURE_COPY`/`BILLING_COPY`/`AUTH_COPY`/`summarizeAttention`)は現状維持**(Masa決定 2026-07-31)。**依頼文の言語に追従するのは仕様**。「UIは英語」を根拠にこれらの `ja` を削除しない。英語化するのは**無条件に日本語が出る固定ラベル**だけ。
|
|
64
|
+
- 全文=`docs/design/DESIGN-RULES.md` §5.5「文章・コンテンツの品」。
|
|
60
65
|
|
|
61
66
|
## プロダクトの不変条件(実装前に読む・2026-07-30 追加)
|
|
62
67
|
|
package/LICENSE.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Galda CLI license
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kodo Inc. All rights reserved.
|
|
4
|
+
|
|
5
|
+
This software is proprietary and source-available. The source is distributed
|
|
6
|
+
so that you can read, audit, and verify what runs on your machine. It is not
|
|
7
|
+
open source.
|
|
8
|
+
|
|
9
|
+
## You may
|
|
10
|
+
|
|
11
|
+
- Install and run this software, for personal or commercial purposes, subject
|
|
12
|
+
to a valid Galda subscription where one is required.
|
|
13
|
+
- Read, inspect, and audit the source code, including for security review.
|
|
14
|
+
- Modify it locally for your own use.
|
|
15
|
+
|
|
16
|
+
## You may not
|
|
17
|
+
|
|
18
|
+
- Redistribute, resell, sublicense, or host this software, in whole or in
|
|
19
|
+
part, whether modified or unmodified.
|
|
20
|
+
- Use the source code to build or operate a competing product or service.
|
|
21
|
+
- Remove or obscure this notice.
|
|
22
|
+
|
|
23
|
+
## No warranty
|
|
24
|
+
|
|
25
|
+
This software is provided "as is", without warranty of any kind, express or
|
|
26
|
+
implied, including but not limited to the warranties of merchantability,
|
|
27
|
+
fitness for a particular purpose, and non-infringement. In no event shall the
|
|
28
|
+
copyright holder be liable for any claim, damages, or other liability arising
|
|
29
|
+
from, out of, or in connection with this software or its use.
|
|
30
|
+
|
|
31
|
+
## Contact
|
|
32
|
+
|
|
33
|
+
https://galda.app
|
package/app/index.html
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">
|
|
6
6
|
<title>Galda</title>
|
|
7
7
|
<script>
|
|
8
8
|
// app/index.html is a server-backed SPA, not a standalone artifact. Rendering the
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
/* Design tokens + light/dark palette live in app/theme.css (design/CDO role).
|
|
32
32
|
Everything below consumes those tokens; never hard-code colors here. */
|
|
33
33
|
*{box-sizing:border-box}
|
|
34
|
-
html,body{height:100
|
|
34
|
+
html,body{height:100%;overflow-x:hidden;overscroll-behavior:none}
|
|
35
35
|
body{margin:0;background:var(--canvas);color:var(--ink);font-family:var(--ui);
|
|
36
36
|
font-size:14px;font-variant-numeric:tabular-nums;-webkit-font-smoothing:antialiased;display:flex;flex-direction:column;overflow:hidden}
|
|
37
37
|
button{font:inherit}
|
|
@@ -333,6 +333,14 @@
|
|
|
333
333
|
color:var(--ink2);font:inherit;font-size:12px;cursor:pointer;text-align:left;width:100%}
|
|
334
334
|
.appopt:hover{background:rgba(var(--tint),.06);color:var(--ink)}
|
|
335
335
|
.appopt.active{color:var(--ink);background:var(--hair2)}
|
|
336
|
+
/* A chooser is already open somewhere: dim the glyph, no surface, and no hover
|
|
337
|
+
answer — the row must not look pressable while it cannot open a second one. */
|
|
338
|
+
.appopt:disabled{cursor:default}
|
|
339
|
+
.appopt:disabled,.appopt:disabled .nm{color:var(--ink3)}
|
|
340
|
+
.appopt:disabled:hover{background:transparent}
|
|
341
|
+
/* the row's own rule sets .nm to full --ink, so say it again at that weight */
|
|
342
|
+
.ctxmenu .appopt.ctxopen:disabled .nm{color:var(--ink3)}
|
|
343
|
+
.ctxmenu .appopt.ctxopen:disabled svg{opacity:.45}
|
|
336
344
|
.appopt svg{width:13px;height:13px;stroke:currentColor;stroke-width:1.7;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
337
345
|
.modelsel{position:relative}
|
|
338
346
|
.modelbtn{font-family:var(--mono);font-size:10.5px;text-transform:capitalize}
|
|
@@ -2325,7 +2333,7 @@
|
|
|
2325
2333
|
.redesign .gh-del .ghm{color:#f85149}
|
|
2326
2334
|
.redesign .gh-hi{border-radius:2px;padding:0 1px}
|
|
2327
2335
|
.redesign .gh-add .gh-hi{background:rgba(46,204,113,.55)} .redesign .gh-del .gh-hi{background:rgba(248,81,73,.5)}
|
|
2328
|
-
/* huge-diff footer (spec §7): "…+N 行 · GitHub
|
|
2336
|
+
/* huge-diff footer (spec §7): "…+N 行 · Open on GitHub ↗" */
|
|
2329
2337
|
.redesign .ghfoot{margin-top:8px;font-family:var(--mono);font-size:11.5px;color:var(--ink3);display:flex;gap:2px;align-items:center;flex-wrap:wrap}
|
|
2330
2338
|
.redesign .ghfoot .ghopen{color:var(--ink2);text-decoration:none}
|
|
2331
2339
|
.redesign .ghfoot .ghopen:hover{color:var(--blue)}
|
|
@@ -2378,6 +2386,10 @@
|
|
|
2378
2386
|
.redesign .txr .w{flex:0 0 46px;font-size:11px;color:var(--ink3)}
|
|
2379
2387
|
.redesign .txr.you .w,.redesign .txr.cl .w{color:var(--ink);font-weight:600}
|
|
2380
2388
|
.redesign .txr .m{flex:1;min-width:0;color:var(--ink2)}
|
|
2389
|
+
/* The existing left-hand Activity / Conversation disclosure owns the full text.
|
|
2390
|
+
When it is closed, its preview is capped at three rendered lines; opening the
|
|
2391
|
+
same › control reveals the unchanged transcript. */
|
|
2392
|
+
.redesign .lacc:not([open]) .lfirst .m{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:3;overflow:hidden}
|
|
2381
2393
|
.redesign .txr.cl .m{color:var(--ink)}
|
|
2382
2394
|
.redesign .txr.tool .m{color:var(--ink2);font-family:var(--mono);font-size:12px}
|
|
2383
2395
|
.redesign .txr.tool .m b{color:var(--ink);font-weight:600}
|
|
@@ -2578,7 +2590,7 @@
|
|
|
2578
2590
|
magnification / uniform brighten (§5.5: whole-panel uniform, no dark tint).
|
|
2579
2591
|
DOM panel backgrounds/shadows/blur are stripped; text reads via R2 "wells"
|
|
2580
2592
|
(local re-blur + tint behind dense text groups) + --tsh. ---- */
|
|
2581
|
-
#glassgl{position:fixed;inset:0;width:
|
|
2593
|
+
#glassgl{position:fixed;inset:0;width:100dvw;height:100dvh;display:none;z-index:-1;pointer-events:none}
|
|
2582
2594
|
body[data-theme="glass"]{background:#0a0a0b}
|
|
2583
2595
|
body[data-theme="glass"][data-layout="flagship"]{background:transparent} /* WebGL draws the bg (photo is NOT the CSS #themebg here) */
|
|
2584
2596
|
body[data-theme="glass"][data-layout="flagship"] #glassgl{display:block}
|
|
@@ -2650,7 +2662,7 @@
|
|
|
2650
2662
|
/* ---- TB縁アニメ canvas (§6.5 確定: 既定=パルス×2番目色・TBのみ周回・Queは含まない).
|
|
2651
2663
|
Same overlay recipe as the flagship #fx: fixed, pointer-transparent, drawn by the
|
|
2652
2664
|
TBEDGE rAF loop (JS) which runs only while an animation is selected. ---- */
|
|
2653
|
-
#fx{position:fixed;inset:0;width:
|
|
2665
|
+
#fx{position:fixed;inset:0;width:100dvw;height:100dvh;pointer-events:none;z-index:9}
|
|
2654
2666
|
/* ---- TB面 (compbg) × flagship: the layout/theme base rules above outrank the generic
|
|
2655
2667
|
preset rule (cascade order), so re-layer the tint over each base surface here at the
|
|
2656
2668
|
end of the sheet. §6.5: TB面は全テーマに重ねられる. ---- */
|
|
@@ -2830,7 +2842,7 @@
|
|
|
2830
2842
|
moves Review/To Do into a right drawer, and exposes project switching from
|
|
2831
2843
|
the top-left current-project button. Desktop flagship (>640px) is untouched. ---- */
|
|
2832
2844
|
@media (max-width: 640px){
|
|
2833
|
-
html,body{height:100dvh}
|
|
2845
|
+
html,body{height:100dvh;overflow:hidden;overscroll-behavior:none;touch-action:pan-y}
|
|
2834
2846
|
body[data-layout="flagship"]{overflow:hidden}
|
|
2835
2847
|
body[data-layout="flagship"] #fsRoot{height:100dvh;flex-direction:column;padding:8px 24px max(14px, calc(env(safe-area-inset-bottom) + 10px));gap:8px;
|
|
2836
2848
|
overflow:hidden;position:relative;--fscenterw:100%} /* Masa: side margins — narrower cards (8→18px) */
|
|
@@ -2882,7 +2894,7 @@
|
|
|
2882
2894
|
/* Masa: composer was too tall — compact it (ChatGPT-like). Controls + send stay on ONE row
|
|
2883
2895
|
(nowrap; the model/agent chips shrink first, the send never shrinks). */
|
|
2884
2896
|
body[data-layout="flagship"] #fsRoot .composer{padding:8px 10px;gap:6px;border-radius:14px}
|
|
2885
|
-
body[data-layout="flagship"] #fsRoot .composer textarea{font-size:
|
|
2897
|
+
body[data-layout="flagship"] #fsRoot .composer textarea{font-size:16px;line-height:1.3;padding:1px 2px 0;max-height:90px}
|
|
2886
2898
|
body[data-layout="flagship"] #fsRoot .crow{flex-wrap:nowrap;gap:6px}
|
|
2887
2899
|
/* collapse the agent button to its icon (drop the "Claude Code" label) so the row fits
|
|
2888
2900
|
one line with the send fully visible; model + effort keep their text. */
|
|
@@ -3051,7 +3063,7 @@
|
|
|
3051
3063
|
<div class="spacer"></div>
|
|
3052
3064
|
<!-- Feedback 7: layout switcher — flip the arrangement live. Increment 1:
|
|
3053
3065
|
現行 / タスク中央 / 3列カンバン (CSS reflow). 中央フロー・1列 = increment 2. -->
|
|
3054
|
-
<div class="laysel" id="laysel" title="
|
|
3066
|
+
<div class="laysel" id="laysel" title="Switch layout">
|
|
3055
3067
|
<button class="laybtn" data-layout="bflow">B・Center-flow</button>
|
|
3056
3068
|
<button class="laybtn" data-layout="flagship">Flagship</button>
|
|
3057
3069
|
<button class="laybtn" data-layout="current">現行</button>
|
|
@@ -3060,7 +3072,7 @@
|
|
|
3060
3072
|
</div>
|
|
3061
3073
|
<!-- theme switch — the 5 release themes, English registered names only (v3 §6.5;
|
|
3062
3074
|
Glass archived 2026-07-07 · legacy "dark" no longer selectable). -->
|
|
3063
|
-
<div class="laysel" id="themesel" title="
|
|
3075
|
+
<div class="laysel" id="themesel" title="Switch theme">
|
|
3064
3076
|
<button class="laybtn" data-theme="studio">Studio</button>
|
|
3065
3077
|
<button class="laybtn" data-theme="gradient">Gradient</button>
|
|
3066
3078
|
<button class="laybtn" data-theme="banff">Banff</button>
|
|
@@ -3348,12 +3360,12 @@
|
|
|
3348
3360
|
<div class="overlay revfloat" id="summaryOverlay">
|
|
3349
3361
|
<div class="modal peramodal">
|
|
3350
3362
|
<div class="perahead">
|
|
3351
|
-
<div class="peratitle"
|
|
3363
|
+
<div class="peratitle">Review summary<span class="peracount" id="peraCount">0</span></div>
|
|
3352
3364
|
<div class="peratabs" id="peraTabs"></div>
|
|
3353
3365
|
<button class="mbtn ghost" id="peraClose">Close</button>
|
|
3354
3366
|
</div>
|
|
3355
3367
|
<div class="peraask">
|
|
3356
|
-
<input type="text" id="peraAsk" placeholder="
|
|
3368
|
+
<input type="text" id="peraAsk" placeholder="Ask… e.g. What's critical now? / Just the failures / Sorted by priority">
|
|
3357
3369
|
<button type="button" class="askbtn" id="peraAskBtn" aria-label="Ask"><svg viewBox="0 0 24 24"><path d="M12 19V5M5 12l7-7 7 7"/></svg></button>
|
|
3358
3370
|
</div>
|
|
3359
3371
|
<div class="peraanswer" id="peraAnswer" hidden></div>
|
|
@@ -3703,7 +3715,7 @@ const TASK_PRIORITIES = ['高', '中', '低'];
|
|
|
3703
3715
|
function prioBadge(t){
|
|
3704
3716
|
const p = TASK_PRIORITIES.includes(t.priority) ? t.priority : '中';
|
|
3705
3717
|
return `<div class="priosel" data-priosel="${t.id}">
|
|
3706
|
-
<button type="button" class="priobtn prio-${p}" data-priobtn="${t.id}" title="
|
|
3718
|
+
<button type="button" class="priobtn prio-${p}" data-priobtn="${t.id}" title="Change priority">${p}</button>
|
|
3707
3719
|
<div class="priomenu" data-priomenu="${t.id}">${TASK_PRIORITIES.map((x) =>
|
|
3708
3720
|
`<button type="button" class="prioopt${x === p ? ' active' : ''}" data-priotask="${t.id}" data-val="${x}">${x}</button>`).join('')}</div>
|
|
3709
3721
|
</div>`;
|
|
@@ -3721,7 +3733,7 @@ const DEFAULT_REVIEW_DEFINITION = {
|
|
|
3721
3733
|
};
|
|
3722
3734
|
// Shown (faint) under Review when no definition is set yet, so it's obvious the
|
|
3723
3735
|
// line is editable and what it's for.
|
|
3724
|
-
const REVIEW_DEF_HINT = '
|
|
3736
|
+
const REVIEW_DEF_HINT = 'e.g. "Write the pull request in English" — set what done means for a goal';
|
|
3725
3737
|
function reviewDefinitionFor(projectId){ return state.reviewDefinitions[projectId] ?? DEFAULT_REVIEW_DEFINITION; }
|
|
3726
3738
|
// A saved definition on disk may predate the reviewCard/language fields (old
|
|
3727
3739
|
// review-definitions.json entries are never re-validated on server load), so
|
|
@@ -4233,7 +4245,8 @@ function saShotsListHtml(shots){
|
|
|
4233
4245
|
if (!list.length) return '';
|
|
4234
4246
|
if (list.length === 1) return saShotFigHtml(list[0]);
|
|
4235
4247
|
const older = list.slice(0, -1);
|
|
4236
|
-
|
|
4248
|
+
const label = `Show ${older.length} older screenshot${older.length === 1 ? '' : 's'}`;
|
|
4249
|
+
return `${saShotFigHtml(list[list.length - 1])}<details class="rshotfold"><summary>${label}</summary>${older.map(saShotFigHtml).join('')}</details>`;
|
|
4237
4250
|
}
|
|
4238
4251
|
function saPlainPairHtml(pair){
|
|
4239
4252
|
const col = (sh, label) => `<div class="ba-col"><span class="balabel">${label}</span><img class="proofimg" src="${esc(sh.url)}" alt="${esc(label)}" loading="lazy"></div>`;
|
|
@@ -4296,7 +4309,7 @@ function reviewDigestHtml(rows){
|
|
|
4296
4309
|
</div>
|
|
4297
4310
|
</div>`;
|
|
4298
4311
|
};
|
|
4299
|
-
return `<div class="digestcard"><div class="digesthead"
|
|
4312
|
+
return `<div class="digestcard"><div class="digesthead">Pending reviews<span class="digestcount">${rows.length}</span></div>
|
|
4300
4313
|
<div class="digestrows">${rows.map(item).join('')}</div></div>`;
|
|
4301
4314
|
}
|
|
4302
4315
|
|
|
@@ -4411,7 +4424,7 @@ function reviewActionsHtml(g){
|
|
|
4411
4424
|
<div class="rvhead"><span class="dot"></span>レビュー — ${esc(sum.headline)} · ${esc(sum.detail)}</div>
|
|
4412
4425
|
<div class="rvwhat">${esc(sum.what)}</div>
|
|
4413
4426
|
<div class="rvitems">${items}</div>
|
|
4414
|
-
<button type="button" class="rvopen reqopen" data-goal="${g.id}"
|
|
4427
|
+
<button type="button" class="rvopen reqopen" data-goal="${g.id}">Open the review checklist →</button>
|
|
4415
4428
|
</div>`;
|
|
4416
4429
|
}
|
|
4417
4430
|
|
|
@@ -4432,7 +4445,7 @@ function goalRow(g){
|
|
|
4432
4445
|
// shouldShowGoalSummary/GOAL_COMPLETE_STATUSES と揃える(そちらは純関数として
|
|
4433
4446
|
// テスト済み — この行はブラウザ側の複製)。
|
|
4434
4447
|
const summary = GOAL_COMPLETE_STATUSES.includes(g.status) && g.summary
|
|
4435
|
-
? `<div class="gsummary"><div class="gslabel"><span class="dot"></span>Manager
|
|
4448
|
+
? `<div class="gsummary"><div class="gslabel"><span class="dot"></span>Manager summary</div>${esc(g.summary)}</div>` : '';
|
|
4436
4449
|
const pr = g.pr ? `<a class="prlink" href="${esc(g.pr)}" target="_blank">${g.status === 'review' ? 'In review — open the Pull Request' : 'Open the Pull Request'}</a>`
|
|
4437
4450
|
: g.prError ? (() => {
|
|
4438
4451
|
const meta = (() => { const raw = g.prErrorClass || inferErrorClass(`PR step failed: ${g.prError}`, g.agent); const C = FAILURE_COPY[failureLang()]; return { ...raw, title: C.infra.title, body: C.infra[raw.reason] || C.infra.pr, copy: C }; })();
|
|
@@ -4464,7 +4477,7 @@ function goalRow(g){
|
|
|
4464
4477
|
function extRow(c){
|
|
4465
4478
|
const short = esc((c.hash ?? '').slice(0, 7));
|
|
4466
4479
|
const when = c.date ? new Date(c.date).toLocaleString() : '';
|
|
4467
|
-
return `<div class="extrow"><span class="exthash">${short}</span><span class="extsubj">${esc(c.subject ?? '')} · Claude Code CLI
|
|
4480
|
+
return `<div class="extrow"><span class="exthash">${short}</span><span class="extsubj">${esc(c.subject ?? '')} · from Claude Code CLI</span>${when ? `<span class="extdate">${esc(when)}</span>` : ''}</div>`;
|
|
4468
4481
|
}
|
|
4469
4482
|
|
|
4470
4483
|
// Mirrors engine/lib.mjs mergeGoalTimeline() — kept in sync manually since
|
|
@@ -4753,7 +4766,7 @@ function renderTasks(){
|
|
|
4753
4766
|
// half-finished item leaves Attention for good instead of piling up. With
|
|
4754
4767
|
// no parent goal we fall back to skipping just the task.
|
|
4755
4768
|
const clear = g
|
|
4756
|
-
? `<button class="retrybtn skip" data-archivegoal="${g.id}" title="
|
|
4769
|
+
? `<button class="retrybtn skip" data-archivegoal="${g.id}" title="Retire this goal (no rework)">Archive</button>`
|
|
4757
4770
|
: `<button class="retrybtn skip" data-skipbtn="${t.id}">Dismiss</button>`;
|
|
4758
4771
|
cond = `${reason} <button class="retrybtn" data-retrybtn="${t.id}">Retry</button> ${clear}`;
|
|
4759
4772
|
} else if (t.reply && g) {
|
|
@@ -4881,7 +4894,7 @@ function renderTasks(){
|
|
|
4881
4894
|
const dAllClear = dGreen === digestCount && !dBlocked;
|
|
4882
4895
|
// S5 summary (deterministic — no LLM). ⤢ opens the ペライチ dashboard above.
|
|
4883
4896
|
const reviewDigest = digestCount
|
|
4884
|
-
? `<div class="s5sum" id="summaryEntry" title="Open the review summary
|
|
4897
|
+
? `<div class="s5sum" id="summaryEntry" title="Open the review summary">
|
|
4885
4898
|
<span class="s5exp"><svg width="13" height="13" viewBox="0 0 24 24"><path d="M15 4h5v5M20 4l-7 7M9 20H4v-5M4 20l7-7"/></svg></span>
|
|
4886
4899
|
<span class="s5ring" style="background:conic-gradient(var(--green) 0 ${dPct}%, var(--hair2) ${dPct}% 100%)"><span class="s5in">${dGreen}/${digestCount}</span></span>
|
|
4887
4900
|
<span class="s5tx"><span class="s5hl">${dAllClear ? 'Ready to review' : `${dGreen} of ${digestCount} ready`}</span><span class="s5sub">${dAllClear ? 'all green' : dGreen === digestCount ? 'blocked needs attention' : 'some failing'} · ${dBlocked ? `${dBlocked} blocked` : 'none blocked'}</span></span>
|
|
@@ -4905,7 +4918,7 @@ function renderTasks(){
|
|
|
4905
4918
|
// (see saItemHtml's carriesBody) instead — the list row stays just the ask.
|
|
4906
4919
|
return `<div class="task revrow" data-goal="${g.id}"><div class="ic pending"></div><div class="tx"><div class="name">${esc(checkLine)} ${goalSourceBadge(g.source)}</div>
|
|
4907
4920
|
<div class="cond">${prChipHtml(g.pr, g.prClosed, 'prbig')}</div>
|
|
4908
|
-
<div class="qreplyrow" data-qreplyrow="${g.id}"${state.qreplyOpen.has(g.id) ? '' : ' hidden'}><input type="text" data-qreplyinput="${g.id}" placeholder="
|
|
4921
|
+
<div class="qreplyrow" data-qreplyrow="${g.id}"${state.qreplyOpen.has(g.id) ? '' : ' hidden'}><input type="text" data-qreplyinput="${g.id}" placeholder="What should change? Sends it back to To Do for rework"><button type="button" class="qreplysend" data-qreplysend="${g.id}" aria-label="Send">${ICONS.up}</button></div></div>
|
|
4909
4922
|
<div class="revquick">
|
|
4910
4923
|
<button type="button" class="qbtn ok" data-qapprove="${g.id}" title="Approve">${ICONS.check}</button>
|
|
4911
4924
|
<button type="button" class="qbtn no" data-qdismiss="${g.id}" title="Send back">${ICONS.close}</button>
|
|
@@ -5057,7 +5070,7 @@ function renderTasks(){
|
|
|
5057
5070
|
// A question keeps the row open: the answer lands under this very box.
|
|
5058
5071
|
if (body.outcome !== 'answer') state.qreplyOpen.delete(Number(gid));
|
|
5059
5072
|
await refresh();
|
|
5060
|
-
} catch { showErr('
|
|
5073
|
+
} catch { showErr('Could not send.'); }
|
|
5061
5074
|
};
|
|
5062
5075
|
for (const b of $('tasklist').querySelectorAll('[data-qreplysend]')) {
|
|
5063
5076
|
b.onclick = (e) => { e.stopPropagation(); sendQReply(b.dataset.qreplysend, $('tasklist').querySelector(`[data-qreplyinput="${b.dataset.qreplysend}"]`)); };
|
|
@@ -5223,8 +5236,8 @@ function openGoalheadReply(id, btn){
|
|
|
5223
5236
|
wrap.appendChild(inp); wrap.appendChild(snd); head.insertAdjacentElement('afterend', wrap); inp.focus();
|
|
5224
5237
|
const send = async () => { const t = inp.value.trim(); if (!t) return;
|
|
5225
5238
|
let r; try { r = await fetch(withKey(`/api/goals/${id}/reply`), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(replyWorkerPayload({ text: t })) }); }
|
|
5226
|
-
catch { showErr('
|
|
5227
|
-
if (!r.ok) showErr('
|
|
5239
|
+
catch { showErr("This goal hasn't started — use Edit instead."); return; }
|
|
5240
|
+
if (!r.ok) showErr("This goal hasn't started — use Edit instead."); await refresh(); };
|
|
5228
5241
|
inp.onkeydown = (e) => { if (e.key === 'Enter' && !e.isComposing) { e.preventDefault(); send(); } if (e.key === 'Escape') wrap.remove(); };
|
|
5229
5242
|
snd.onclick = send;
|
|
5230
5243
|
}
|
|
@@ -5249,8 +5262,8 @@ function wirePriority(){
|
|
|
5249
5262
|
try {
|
|
5250
5263
|
const r = await fetch(withKey(`/api/tasks/${b.dataset.priotask}/priority`), { method: 'PUT',
|
|
5251
5264
|
headers: { 'content-type': 'application/json' }, body: JSON.stringify({ priority: b.dataset.val }) });
|
|
5252
|
-
if (!r.ok) showErr('
|
|
5253
|
-
} catch { showErr('
|
|
5265
|
+
if (!r.ok) showErr('Could not change the priority.'); else await refresh();
|
|
5266
|
+
} catch { showErr('Could not change the priority.'); }
|
|
5254
5267
|
};
|
|
5255
5268
|
}
|
|
5256
5269
|
}
|
|
@@ -5395,21 +5408,21 @@ function renderRdEditor(){
|
|
|
5395
5408
|
<span class="rdhint">Any task whose headless-browser check didn't pass (or never ran) forces Review too.</span></span></label>
|
|
5396
5409
|
<label class="rdrow rddesc"><span><span class="rdlabel">Review instructions</span>
|
|
5397
5410
|
<span class="rdhint">Goes into the pull request body, so whoever reviews it knows what to look for. The worker doesn't read this while it works — instructions for the AI belong in your project's CLAUDE.md / AGENTS.md.</span>
|
|
5398
|
-
<textarea class="rdtext" id="rdDescription" maxlength="500" rows="2" placeholder="
|
|
5411
|
+
<textarea class="rdtext" id="rdDescription" maxlength="500" rows="2" placeholder="e.g. "Compare the before/after screenshots and check the spacing lines up" — what the reviewer should look at">${esc(state.rdDraft.description ?? '')}</textarea></span></label>
|
|
5399
5412
|
|
|
5400
|
-
<div class="rdsub"
|
|
5413
|
+
<div class="rdsub">Review card fields</div>
|
|
5401
5414
|
<label class="rdrow"><input type="checkbox" id="rdCardScreenshots"${state.rdDraft.reviewCard.screenshots ? ' checked' : ''}>
|
|
5402
|
-
<span><span class="rdlabel"
|
|
5403
|
-
<span class="rdhint"
|
|
5415
|
+
<span><span class="rdlabel">Screenshots</span>
|
|
5416
|
+
<span class="rdhint">Show the evidence (screenshots/GIFs) on the review card.</span></span></label>
|
|
5404
5417
|
|
|
5405
|
-
<div class="rdsub"
|
|
5418
|
+
<div class="rdsub">Output language</div>
|
|
5406
5419
|
<label class="rdrow"><select id="rdLanguage">
|
|
5407
5420
|
<option value="auto"${state.rdDraft.language === 'auto' ? ' selected' : ''}>Auto (match request)</option>
|
|
5408
5421
|
<option value="ja"${state.rdDraft.language === 'ja' ? ' selected' : ''}>日本語</option>
|
|
5409
5422
|
<option value="en"${state.rdDraft.language === 'en' ? ' selected' : ''}>English</option>
|
|
5410
5423
|
</select>
|
|
5411
5424
|
<span><span class="rdlabel">Review language</span>
|
|
5412
|
-
<span class="rdhint"
|
|
5425
|
+
<span class="rdhint">Language for the text the Manager writes, such as the review summary (What to check / What changed).</span></span></label>`;
|
|
5413
5426
|
$('rdDefaultWantsPR').addEventListener('change', (e) => { state.rdDraft.defaultWantsPR = e.target.checked; });
|
|
5414
5427
|
$('rdRequireVerify').addEventListener('change', (e) => { state.rdDraft.requireVerifyPass = e.target.checked; });
|
|
5415
5428
|
$('rdDescription').addEventListener('input', (e) => { state.rdDraft.description = e.target.value; });
|
|
@@ -6315,7 +6328,7 @@ async function sendReviewDismiss(text){
|
|
|
6315
6328
|
// these paths, so passing them in the reply text is enough — no API change.
|
|
6316
6329
|
const imgs = reviewAttach.slice();
|
|
6317
6330
|
const fullText = imgs.length
|
|
6318
|
-
? `${text}${text ? '\n\n' : ''}
|
|
6331
|
+
? `${text}${text ? '\n\n' : ''}Attached images (the worker can open these paths with Read): ${imgs.map((a) => a.path).join(', ')}`
|
|
6319
6332
|
: text;
|
|
6320
6333
|
try {
|
|
6321
6334
|
const body = await replyToGoal(goal.id, fullText, '#reviewFixInput');
|
|
@@ -7204,7 +7217,7 @@ function renderFsBoard(){
|
|
|
7204
7217
|
// flagship mock; a new goal enters via the composer). Earlier this was To-Do-only, which read
|
|
7205
7218
|
// as "position is off" against the Artifact (Masa 2026-07-18).
|
|
7206
7219
|
const col = (name, stg, count, html, stage) =>
|
|
7207
|
-
`<section class="fsbcol" data-stage="${stage}" style="--stg:${stg}"><div class="fsbcolhd"><span class="fsbdot"></span><span class="fsbcolnm">${name}</span>${stage === 'doing' ? `<button class="fsbcpause" data-fspause="1" aria-pressed="${paused ? 'true' : 'false'}" title="${paused ? 'Resume
|
|
7220
|
+
`<section class="fsbcol" data-stage="${stage}" style="--stg:${stg}"><div class="fsbcolhd"><span class="fsbdot"></span><span class="fsbcolnm">${name}</span>${stage === 'doing' ? `<button class="fsbcpause" data-fspause="1" aria-pressed="${paused ? 'true' : 'false'}" title="${paused ? 'Resume' : 'Pause'}">${paused ? '<svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>' : '<svg viewBox="0 0 24 24"><rect x="6" y="5" width="4" height="14" rx="1"/><rect x="14" y="5" width="4" height="14" rx="1"/></svg>'}</button>` : ''}<span class="fsbcnt">${count}</span><button class="fsbcadd" data-fsnew="1" title="New goal"><svg viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></svg></button></div><div class="fsbbody">${html || '<div class="fsbempty">—</div>'}</div><button class="fsbadd" data-fsnew="1" title="New goal"><span class="paplus"></span>New</button></section>`;
|
|
7208
7221
|
// Depth card (CDO, Masa 2026-07-17): #NN + title, colour on the dot only, and a continuous
|
|
7209
7222
|
// stroke + n/m ONLY while the goal is actually in flight. The column already names the
|
|
7210
7223
|
// status, so the card never repeats it. Stopped/failed carry NO bar (a dead bar would claim
|
|
@@ -7412,7 +7425,7 @@ function renderFsReview(){
|
|
|
7412
7425
|
const rnote = g0?.retest?.passed ? `<span class="rnote">✓ ${esc(g0.retest.note)}</span>` : '';
|
|
7413
7426
|
const incompleteNote = r.incompleteRequirementCount ? `<span class="rnote">${r.incompleteRequirementCount} requirement${r.incompleteRequirementCount === 1 ? '' : 's'} need review</span>` : '';
|
|
7414
7427
|
const infraNote = r.verificationInfraError?.detail
|
|
7415
|
-
? `<span class="rnote"
|
|
7428
|
+
? `<span class="rnote">Verification system error · <button type="button" class="retrybtn" data-goalreverify="${r.goalIds[0]}">Re-verify</button></span>`
|
|
7416
7429
|
: '';
|
|
7417
7430
|
const conflictNote = g0?.conflictsWith?.length
|
|
7418
7431
|
? `<span class="rstat conflict">Potential conflict with ${esc(g0.conflictsWith.map((id) => `#${id}`).join(', '))}</span>` : '<span class="rstat">In review</span>';
|
|
@@ -7660,7 +7673,7 @@ function renderFsTodo(){
|
|
|
7660
7673
|
}).join('')
|
|
7661
7674
|
+ finalizingGoals.map((g) => `<div class="drow"><div class="trow now" data-fsgoalrow="${g.id}"><span class="st run"></span>${noSpan(g.id)}<span class="tt">${tt(g.reviewSummary?.check || g.plan?.[0] || g.text, 60)}</span><span class="stword wait">Finalizing</span>${fsActs(g.id)}</div></div>`).join('')
|
|
7662
7675
|
// 一時停止したゴールは Doing に「Paused」1行(アンバー)で留める+Resume。走行/中断行は上と needsRows で抑止済み。
|
|
7663
|
-
+ pgoals.filter((g) => pausedGoalIds.has(g.id)).map((g) => `<div class="drow"><div class="trow now" data-fsgoalrow="${g.id}"><span class="st run int"></span>${noSpan(g.id)}<span class="tt">${tt(g.reviewSummary?.check || g.plan?.[0] || g.text, 60)}</span><span class="stword int">Paused</span><button class="ab txt" data-fspause="1" title="Resume
|
|
7676
|
+
+ pgoals.filter((g) => pausedGoalIds.has(g.id)).map((g) => `<div class="drow"><div class="trow now" data-fsgoalrow="${g.id}"><span class="st run int"></span>${noSpan(g.id)}<span class="tt">${tt(g.reviewSummary?.check || g.plan?.[0] || g.text, 60)}</span><span class="stword int">Paused</span><button class="ab txt" data-fspause="1" title="Resume">Resume</button></div></div>`).join('');
|
|
7664
7677
|
// NEEDS YOU: stopped/errored work that used to fold into Doing, now under its own caption so
|
|
7665
7678
|
// it no longer reads as "still in flight" (Masa 2026-07-19). Three kinds share it, each
|
|
7666
7679
|
// keeping the affordances it already had — nothing about the failed-task and blocked-goal
|
|
@@ -7743,7 +7756,7 @@ function renderFsTodo(){
|
|
|
7743
7756
|
: `<span class="num fstdlabel">${nextCount} next · ${doingCount} doing${later.length ? ` · ${later.length} later` : ''}</span>`;
|
|
7744
7757
|
el.innerHTML = `<div class="lhd sechd" id="fsTodoHd"><span class="hd fstdlabel">To Do</span>${_todoNum}<span class="fold fstdlabel">▾</span></div>
|
|
7745
7758
|
<div class="tl">
|
|
7746
|
-
${(doingRows || paused) ? `<span class="tlcap tlcap-doing" style="padding-top:0">Doing<button class="tlcap-pause" data-fspause="1" aria-pressed="${paused ? 'true' : 'false'}" title="${paused ? 'Resume
|
|
7759
|
+
${(doingRows || paused) ? `<span class="tlcap tlcap-doing" style="padding-top:0">Doing<button class="tlcap-pause" data-fspause="1" aria-pressed="${paused ? 'true' : 'false'}" title="${paused ? 'Resume' : 'Pause'}">${paused ? '<svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>' : '<svg viewBox="0 0 24 24"><rect x="6" y="5" width="4" height="14" rx="1"/><rect x="14" y="5" width="4" height="14" rx="1"/></svg>'}</button></span>${doingRows}` : ''}
|
|
7747
7760
|
${needsRows ? `<span class="tlcap needs"${(doingRows || paused) ? '' : ' style="padding-top:0"'}>Needs you</span>${needsRows}` : ''}
|
|
7748
7761
|
${rejected.length ? `<span class="tlcap rejcap"${(doingRows || paused) || needsRows ? '' : ' style="padding-top:0"'}>Dismissed<span class="num">${rejected.length}</span></span>${rejected.map((g) => `<div class="trow up rej" data-fsgoalrow="${g.id}"><span class="st rej"></span>${noSpan(g.id)}<span class="tt">${tt(g.text, 70)}</span><span class="acts"><button class="ab" data-fsreopen="${g.id}" title="Reopen — back to Review">${FS_ICONS.reopen}</button><button class="ab" data-fsrejdel="${g.id}" title="Delete — discard (closes the PR)">${FS_ICONS.del}</button></span></div>`).join('')}` : ''}
|
|
7749
7762
|
${(parkedRows || upRows) ? `<span class="tlcap"${(doingRows || paused) || needsRows || rejected.length ? '' : ' style="padding-top:0"'}>Next up</span>${parkedRows}${upRows}` : ''}
|
|
@@ -7833,8 +7846,8 @@ function renderFsTodo(){
|
|
|
7833
7846
|
for (const b of el.querySelectorAll('[data-fsreply]')) b.onclick = (e) => { e.stopPropagation();
|
|
7834
7847
|
fsInlineInput(b.closest('.trow'), '', 'Reply to this goal…', async (text) => {
|
|
7835
7848
|
let r; try { r = await fetch(withKey(`/api/goals/${b.dataset.fsreply}/reply`), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(replyWorkerPayload({ text })) }); }
|
|
7836
|
-
catch { showErr('
|
|
7837
|
-
if (!r.ok) showErr('
|
|
7849
|
+
catch { showErr("This goal hasn't started — use Edit instead."); return; }
|
|
7850
|
+
if (!r.ok) showErr("This goal hasn't started — use Edit instead."); await refresh();
|
|
7838
7851
|
}); };
|
|
7839
7852
|
for (const b of el.querySelectorAll('[data-fsedit]')) b.onclick = (e) => { e.stopPropagation();
|
|
7840
7853
|
const g = state.goals.find((x) => x.id === Number(b.dataset.fsedit));
|
|
@@ -9392,7 +9405,7 @@ async function send(){
|
|
|
9392
9405
|
if (bind) {
|
|
9393
9406
|
const batt = state.attach.slice();
|
|
9394
9407
|
const fullText = batt.length
|
|
9395
|
-
? `${text}${text ? '\n\n' : ''}
|
|
9408
|
+
? `${text}${text ? '\n\n' : ''}Attached images (the worker can open these paths with Read): ${batt.map((a) => a.path).join(', ')}`
|
|
9396
9409
|
: text;
|
|
9397
9410
|
// A bound send belongs to this goal's persisted thread. Do not also add it
|
|
9398
9411
|
// to the center conversation feed: that made a task reply appear below the
|
|
@@ -9479,14 +9492,14 @@ async function askQuestion(){
|
|
|
9479
9492
|
if (!q) return;
|
|
9480
9493
|
const btn = $('peraAskBtn'), out = $('peraAnswer');
|
|
9481
9494
|
inp.disabled = true; if (btn) btn.disabled = true;
|
|
9482
|
-
out.hidden = false; out.className = 'peraanswer loading'; out.textContent = '
|
|
9495
|
+
out.hidden = false; out.className = 'peraanswer loading'; out.textContent = 'Thinking…';
|
|
9483
9496
|
try {
|
|
9484
9497
|
const r = await fetch(withKey('/api/ask'), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ question: q }) });
|
|
9485
9498
|
const body = await r.json().catch(() => ({}));
|
|
9486
|
-
if (!r.ok) throw new Error(body.error || '
|
|
9487
|
-
out.className = 'peraanswer'; out.textContent = body.answer || '(
|
|
9499
|
+
if (!r.ok) throw new Error(body.error || 'Could not ask.');
|
|
9500
|
+
out.className = 'peraanswer'; out.textContent = body.answer || '(no answer)';
|
|
9488
9501
|
} catch (e) {
|
|
9489
|
-
out.className = 'peraanswer error'; out.textContent = e.message || '
|
|
9502
|
+
out.className = 'peraanswer error'; out.textContent = e.message || 'Could not ask.';
|
|
9490
9503
|
} finally {
|
|
9491
9504
|
inp.disabled = false; if (btn) btn.disabled = false;
|
|
9492
9505
|
}
|
|
@@ -9513,8 +9526,8 @@ function peraThumb(r, cls = ''){
|
|
|
9513
9526
|
}
|
|
9514
9527
|
function peraActs(r){
|
|
9515
9528
|
const ids = r.goalIds.join(',');
|
|
9516
|
-
const okTitle = r.plan ? 'Approve plan — execute' : '
|
|
9517
|
-
return `<span class="pgo"><button class="pbtn ok" data-peraok="${ids}" title="${okTitle}">✓</button><button class="pbtn no" data-perano="${ids}" title="
|
|
9529
|
+
const okTitle = r.plan ? 'Approve plan — execute' : 'Approve';
|
|
9530
|
+
return `<span class="pgo"><button class="pbtn ok" data-peraok="${ids}" title="${okTitle}">✓</button><button class="pbtn no" data-perano="${ids}" title="Send back">✕</button></span>`;
|
|
9518
9531
|
}
|
|
9519
9532
|
// Plan-review body: the proposed plan, shown verbatim in the card so the
|
|
9520
9533
|
// reviewer approves (→ execute) or dismisses (→ revise) with the plan in view.
|
|
@@ -9674,7 +9687,7 @@ function latestGoalRunForOutcome(goalId, tasks, outcome){
|
|
|
9674
9687
|
const cutoff = outcome ? taskSortTime(outcome) : Infinity;
|
|
9675
9688
|
const prev = runs.find((t) => t.id !== outcome?.id && taskSortTime(t) <= cutoff) || (!outcome ? runs[0] : null);
|
|
9676
9689
|
if (!prev) return null;
|
|
9677
|
-
const note = '
|
|
9690
|
+
const note = 'Inherited previous preview: the latest reply did not declare a new preview.';
|
|
9678
9691
|
return {
|
|
9679
9692
|
taskId: prev.id,
|
|
9680
9693
|
attemptId: taskAttemptId(outcome) || taskAttemptId(prev),
|
|
@@ -9928,7 +9941,7 @@ function saLedgerRows(){
|
|
|
9928
9941
|
// 3-for-2 replacement, an add with no matching del, a rewritten line where almost
|
|
9929
9942
|
// nothing survives — all keep today's behaviour exactly (tint only, no span).
|
|
9930
9943
|
// How many body rows the diff really has, ignoring the display cap. A big change used
|
|
9931
|
-
// to be cut at 80 rows with nothing said (SPEC §7 asks for "…+N 行 · GitHub
|
|
9944
|
+
// to be cut at 80 rows with nothing said (SPEC §7 asks for "…+N 行 · Open on GitHub ↗"
|
|
9932
9945
|
// precisely so a reviewer is never shown a slice that looks like the whole thing).
|
|
9933
9946
|
// Same skip rules as saUnifiedToGh below — headers and @@ markers are not rows.
|
|
9934
9947
|
function saDiffRowCount(diff){
|
|
@@ -10092,7 +10105,7 @@ function saMsgRowsHtml(msgs, expanded){
|
|
|
10092
10105
|
}
|
|
10093
10106
|
|
|
10094
10107
|
// Structured GitHub diff (REVIEW-CARD-SPEC §7): renders {o,n,t,c,h} rows with a
|
|
10095
|
-
// word-level highlight (h) and, for a huge change, a "…+N 行 · GitHub
|
|
10108
|
+
// word-level highlight (h) and, for a huge change, a "…+N 行 · Open on GitHub ↗"
|
|
10096
10109
|
// footer (only linking to GitHub when a PR exists). Mirrors the flagship artifact's
|
|
10097
10110
|
// ghDiffHtml so the app pixel-matches. `more` = extra unshown line count; `pr` = url.
|
|
10098
10111
|
// Request fold threshold (Masa 2026-07-27): Request is supposed to be the user's short
|
|
@@ -10128,7 +10141,7 @@ function saGhDiffRowsHtml(rows, more, pr, goalId){
|
|
|
10128
10141
|
} else if (l.h) { const h = esc(l.h); code = code.replace(h, `<span class="gh-hi">${h}</span>`); }
|
|
10129
10142
|
return `<div class="ghl ${cls}"><span class="gho">${l.o ?? ''}</span><span class="ghn">${l.n ?? ''}</span><span class="ghm">${mk}</span><span class="ghc">${code}</span></div>`;
|
|
10130
10143
|
}).join('');
|
|
10131
|
-
const foot = more ? `<div class="ghfoot"><span>…+${more}
|
|
10144
|
+
const foot = more ? `<div class="ghfoot"><span>…+${more} more lines</span>${pr ? ` · <a class="ghopen" href="${esc(pr)}" target="_blank" rel="noopener">Open on GitHub ↗</a>` : ''}</div>` : '';
|
|
10132
10145
|
if (goalId != null && rows.length > SA_DIFF_FOLD_OVER) {
|
|
10133
10146
|
const open = SA.diffOpen.has(goalId);
|
|
10134
10147
|
const label = open ? 'Collapse' : `Show all <span class="n">${rows.length}</span> lines`;
|
|
@@ -10441,7 +10454,7 @@ function saItemHtml(it, i){
|
|
|
10441
10454
|
const reportBody = it.report ? `<div class="rreport">${mdLite(it.report, { images: false })}</div>` : '';
|
|
10442
10455
|
const infra = it.verificationInfraError;
|
|
10443
10456
|
const infraBody = infra?.detail
|
|
10444
|
-
? `<div class="rinfra"><p class="rbody">${esc(infra.detail)}</p><button type="button" class="retrybtn" data-saact="reverify:${i}"
|
|
10457
|
+
? `<div class="rinfra"><p class="rbody">${esc(infra.detail)}</p><button type="button" class="retrybtn" data-saact="reverify:${i}">Re-verify</button></div>`
|
|
10445
10458
|
: '';
|
|
10446
10459
|
// ── Screenshots (P2, Masa 2026-07-22): the pictures the AI took of its own result.
|
|
10447
10460
|
// Full width, at their own aspect — a four-variant comparison sheet is unreadable
|
|
@@ -10465,7 +10478,7 @@ function saItemHtml(it, i){
|
|
|
10465
10478
|
if (it.recording) proofInner += saVideoFigHtml(it.recordingUrl);
|
|
10466
10479
|
}
|
|
10467
10480
|
// ── What changed (spec §4/§7): description + files summary (always) + the diff, which
|
|
10468
|
-
// peeks then "…+N 行 · GitHub
|
|
10481
|
+
// peeks then "…+N 行 · Open on GitHub ↗" for a huge change (no GitHub link when there's no
|
|
10469
10482
|
// PR). Structured recorded rows (ghRows) win over a git/client unified diff. Dropped
|
|
10470
10483
|
// entirely for a conversational task. ──
|
|
10471
10484
|
// No prose here (P1, Masa 2026-07-22): reviewSummary.changed was the intake AI writing
|
|
@@ -11471,7 +11484,9 @@ function renderCtxBar(){
|
|
|
11471
11484
|
// LRM, so the rtl clipping trims the HEAD of the path without reversing the text
|
|
11472
11485
|
+ `<span class="pt">\u200e${esc(folderShort(r.dir))}</span></button>`)
|
|
11473
11486
|
.join('');
|
|
11474
|
-
|
|
11487
|
+
// Redrawing the menu must not hand back a second chooser: carry the disabled
|
|
11488
|
+
// state that setFolderPickerBusy() put on the live button.
|
|
11489
|
+
const openRow = `<button type="button" class="appopt ctxopen${folderPickerBusy ? ' busy' : ''}" id="ctxopenfolder"${folderPickerBusy ? ' disabled' : ''} 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>`;
|
|
11475
11490
|
menu.innerHTML = `${opts}${openRow}<div class="ctxmsg" id="ctxdirmsg"></div>`;
|
|
11476
11491
|
}
|
|
11477
11492
|
// Workspace mode is fixed to the isolated worktree (clean separate checkout). The
|
|
@@ -11517,9 +11532,22 @@ async function setProjectFolder(dir){
|
|
|
11517
11532
|
// project at the chosen path. This is also correct through the authenticated
|
|
11518
11533
|
// relay: it forwards the request to the account owner's loopback engine, so the
|
|
11519
11534
|
// picker opens on that owner's Mac rather than on the hosted relay.
|
|
11535
|
+
// One chooser at a time. The dialog opens over in the engine process and can take
|
|
11536
|
+
// a moment to appear (and may not come to the front at all), so a second tap used
|
|
11537
|
+
// to open a second dialog: ten taps left someone with ten dialogs to dismiss. The
|
|
11538
|
+
// request stays in flight for as long as the dialog is up, so "a request is out"
|
|
11539
|
+
// is exactly "a dialog is open" — no timer to guess with.
|
|
11540
|
+
let folderPickerBusy = false;
|
|
11541
|
+
function setFolderPickerBusy(busy){
|
|
11542
|
+
folderPickerBusy = busy;
|
|
11543
|
+
const btn = $('ctxopenfolder');
|
|
11544
|
+
if (btn) { btn.disabled = busy; btn.classList.toggle('busy', busy); }
|
|
11545
|
+
}
|
|
11520
11546
|
async function openFolderNative(){
|
|
11547
|
+
if (folderPickerBusy) return;
|
|
11521
11548
|
const msg = $('ctxdirmsg');
|
|
11522
11549
|
const say = (t, bad) => { if (msg) { msg.textContent = t; msg.classList.toggle('bad', Boolean(bad)); } };
|
|
11550
|
+
setFolderPickerBusy(true);
|
|
11523
11551
|
say('Opening the folder chooser…');
|
|
11524
11552
|
try {
|
|
11525
11553
|
const r = await fetch(withKey('/api/choose-folder'), { method: 'POST' });
|
|
@@ -11529,6 +11557,7 @@ async function openFolderNative(){
|
|
|
11529
11557
|
if (body.unsupported) { say('This engine can’t open a folder dialog on this system.', true); return; }
|
|
11530
11558
|
say(body.error || 'Could not open the folder chooser.', true);
|
|
11531
11559
|
} catch { say('Could not reach the engine.', true); }
|
|
11560
|
+
finally { setFolderPickerBusy(false); }
|
|
11532
11561
|
}
|
|
11533
11562
|
$('ctxdirbtn').onclick = (e) => {
|
|
11534
11563
|
e.stopPropagation();
|
package/bin/manager-for-ai.mjs
CHANGED
|
@@ -149,21 +149,56 @@ await import(pathToFileURL(join(ROOT, 'engine', 'server.mjs')).href);
|
|
|
149
149
|
// stable `<id>.<domain>` from any device. Local-first is preserved — the agent
|
|
150
150
|
// still runs here; the relay is only a door. Opt-in via RELAY_URL for now; the
|
|
151
151
|
// public build will default it to the deployed relay. See relay/DEPLOY.md.
|
|
152
|
+
// The user can turn remote access OFF in Settings (docs/SECURITY.md). That is
|
|
153
|
+
// the discoverable form of `RELAY_URL=`: no relay child, no outbound socket.
|
|
154
|
+
// The switch is written by the SERVER process, so we read the file here and
|
|
155
|
+
// watch it, letting the toggle take effect without restarting the CLI.
|
|
156
|
+
const { readRelayPref, resolveRelayEnabled, RELAY_PREF_FILE } =
|
|
157
|
+
await import(pathToFileURL(join(ROOT, 'engine', 'relay-pref.mjs')).href);
|
|
158
|
+
// Same resolution the server and relay-client use, spelled out here because the
|
|
159
|
+
// DATA_DIR above is scoped to the single-instance check.
|
|
160
|
+
const RELAY_DATA_DIR = process.env.MANAGER_HOME
|
|
161
|
+
?? (existsSync(join(ROOT, '.git')) ? join(ROOT, 'engine') : join(homedir(), '.manager-for-ai'));
|
|
162
|
+
|
|
152
163
|
if (process.env.RELAY_URL) {
|
|
153
164
|
const { spawn } = await import('node:child_process');
|
|
154
165
|
const { superviseRelay } = await import(pathToFileURL(join(ROOT, 'engine', 'relay-supervisor.mjs')).href);
|
|
166
|
+
const { watch } = await import('node:fs');
|
|
167
|
+
|
|
168
|
+
let sup = null; // non-null exactly while the relay child is supervised
|
|
169
|
+
|
|
155
170
|
// Supervise the relay-client: if the CHILD exits (uncaught throw, single-
|
|
156
171
|
// instance lock refuse) we bring it back with backoff, otherwise the app goes
|
|
157
172
|
// silently offline ("Unsent"). A tight crash loop (a config/lock error) trips a
|
|
158
173
|
// circuit breaker so we stop hammering and say why. relay-client heals its own
|
|
159
174
|
// dropped socket; this heals the whole process dying. See engine/relay-supervisor.mjs.
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
175
|
+
const startRelay = () => {
|
|
176
|
+
if (sup) return;
|
|
177
|
+
sup = superviseRelay({
|
|
178
|
+
spawn: () => spawn(process.execPath, [join(ROOT, 'engine', 'relay-client.mjs')], { stdio: 'inherit', env: process.env }),
|
|
179
|
+
log: ({ code, signal, delayMs }) => say(`relay-client exited (code ${code ?? 'n/a'}${signal ? `, ${signal}` : ''}) — restarting in ${Math.round(delayMs / 1000)}s`),
|
|
180
|
+
onGiveUp: () => say('relay-client keeps exiting immediately — giving up; run `npx @galda/cli --signin` or check RELAY_URL'),
|
|
181
|
+
});
|
|
182
|
+
say(`relay: connecting to ${process.env.RELAY_URL} — your fixed URL goes live once you sign in`);
|
|
183
|
+
};
|
|
184
|
+
const stopRelay = () => { sup?.stop(); sup = null; };
|
|
185
|
+
|
|
186
|
+
const applyPref = () => {
|
|
187
|
+
const want = resolveRelayEnabled({ relayUrl: process.env.RELAY_URL, pref: readRelayPref(RELAY_DATA_DIR) });
|
|
188
|
+
if (want && !sup) startRelay();
|
|
189
|
+
else if (!want && sup) { stopRelay(); say('relay: remote access turned off in Settings — no outbound connection'); }
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
applyPref();
|
|
193
|
+
if (!sup) say('relay: remote access is off (Settings) — running fully local');
|
|
194
|
+
|
|
195
|
+
// The switch is written by the SERVER process (POST /api/relay-pref), so the
|
|
196
|
+
// only way we learn about a click is to watch the file. Same cross-process
|
|
197
|
+
// idiom as relay-status.json / relay-takeover.req.
|
|
198
|
+
try {
|
|
199
|
+
watch(RELAY_DATA_DIR, (_e, name) => { if (name === RELAY_PREF_FILE) applyPref(); }).unref();
|
|
200
|
+
} catch { /* watching is a nicety: the choice still applies on next start */ }
|
|
201
|
+
|
|
202
|
+
process.on('exit', stopRelay);
|
|
203
|
+
for (const sig of ['SIGINT', 'SIGTERM']) process.on(sig, () => { stopRelay(); process.exit(0); });
|
|
169
204
|
}
|
package/docs/SECURITY.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# Security and permissions
|
|
2
|
+
|
|
3
|
+
What Galda runs on your machine, what it can touch, and what leaves it.
|
|
4
|
+
|
|
5
|
+
Every claim below is a statement about the code in this package. Source
|
|
6
|
+
references are given so you can check them yourself in the published tarball
|
|
7
|
+
(`npm pack @galda/cli`).
|
|
8
|
+
|
|
9
|
+
日本語の要約は末尾の「日本語」節にあります。
|
|
10
|
+
|
|
11
|
+
## What Galda is
|
|
12
|
+
|
|
13
|
+
Galda does not contain an AI model and does not talk to one. It starts the
|
|
14
|
+
Claude Code or Codex CLI that is already installed and signed in on your
|
|
15
|
+
machine, and gives you a place to queue work and review the result.
|
|
16
|
+
|
|
17
|
+
That has a direct consequence for your code: **your code is never uploaded to
|
|
18
|
+
Galda for inference.** Claude Code and Codex connect to Anthropic or OpenAI
|
|
19
|
+
directly, on your own subscription. Galda is not in that path and never sees
|
|
20
|
+
that traffic.
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
**Galda has no install scripts.** `package.json` declares only `start` and
|
|
25
|
+
`test`; there is no `preinstall`, `install`, `postinstall`, or `prepare`.
|
|
26
|
+
Nothing in this package executes as a side effect of `npm install` or `npx`.
|
|
27
|
+
Code runs only when you start Galda yourself.
|
|
28
|
+
|
|
29
|
+
To read the code before running it:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
npm pack @galda/cli # downloads the tarball, runs nothing
|
|
33
|
+
tar -xzf galda-cli-*.tgz # everything Galda ships is plain JavaScript
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## The local server
|
|
37
|
+
|
|
38
|
+
Galda runs an HTTP server on your machine.
|
|
39
|
+
|
|
40
|
+
- It binds to **loopback only** (`127.0.0.1` and `::1`), so it is not reachable
|
|
41
|
+
from your network. See `engine/server.mjs`.
|
|
42
|
+
- Every request requires an access key stored at `~/.manager-for-ai/secret.key`.
|
|
43
|
+
- All state lives in `~/.manager-for-ai/`. Nothing is written elsewhere.
|
|
44
|
+
|
|
45
|
+
To remove Galda completely:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
rm -rf ~/.manager-for-ai
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
That is the whole uninstall. Galda installs no daemons, no login items, no
|
|
52
|
+
kernel extensions, and no browser extensions.
|
|
53
|
+
|
|
54
|
+
## What the agent is allowed to do
|
|
55
|
+
|
|
56
|
+
Galda does not hand Claude Code or Codex a blank cheque. The tool set is an
|
|
57
|
+
explicit allowlist (`engine/lib.mjs`, `WORKER_TOOLS`).
|
|
58
|
+
|
|
59
|
+
**The agent can:**
|
|
60
|
+
|
|
61
|
+
- Read and edit files, through the `Edit` / `Write` / `Read` / `Glob` / `Grep`
|
|
62
|
+
tools, inside the working copy for the task
|
|
63
|
+
- Run named commands only: language runtimes and package managers (`node`,
|
|
64
|
+
`npm`, `npx`, `pnpm`, `yarn`, `bun`, `deno`, `python`, `pip`, `go`, `cargo`,
|
|
65
|
+
`ruby`, `make`), test and lint runners (`pytest`, `jest`, `vitest`, `tsc`,
|
|
66
|
+
`eslint`, `prettier`), and read-only shell utilities (`cat`, `ls`, `grep`,
|
|
67
|
+
`find`, `diff`, and similar)
|
|
68
|
+
- Read the web (`WebFetch`, `WebSearch`)
|
|
69
|
+
|
|
70
|
+
**The agent cannot:**
|
|
71
|
+
|
|
72
|
+
- Run arbitrary shell commands. `Bash` is not granted; only the named commands
|
|
73
|
+
above are, each as an explicit allowlist entry
|
|
74
|
+
- **Commit, push, checkout, reset, or otherwise change your repository state.**
|
|
75
|
+
Git is granted read-only: `git status`, `git log`, `git diff`, `git show`,
|
|
76
|
+
`git branch`, `git blame`, `git ls-files`, `git rev-parse`. Nothing else
|
|
77
|
+
- Edit files through the shell. File changes go through the `Edit` / `Write`
|
|
78
|
+
tools, so every change appears in the diff you review
|
|
79
|
+
|
|
80
|
+
**Work is isolated from your working tree.** Each task runs in a throwaway git
|
|
81
|
+
worktree, not in the checkout you have open. Your branch, your staged changes,
|
|
82
|
+
and your uncommitted work are not touched.
|
|
83
|
+
|
|
84
|
+
## What leaves your machine
|
|
85
|
+
|
|
86
|
+
Three separate paths. They are worth keeping apart.
|
|
87
|
+
|
|
88
|
+
### 1. Model traffic — does not involve Galda
|
|
89
|
+
|
|
90
|
+
Claude Code and Codex talk to Anthropic and OpenAI directly, using the
|
|
91
|
+
subscription already configured on your machine. Galda neither proxies nor
|
|
92
|
+
observes this. Their data handling is governed by their terms, not ours.
|
|
93
|
+
|
|
94
|
+
### 2. The relay — on by default, and you can turn it off
|
|
95
|
+
|
|
96
|
+
By default Galda opens an outbound WebSocket to `app.galda.app`. This is what
|
|
97
|
+
lets you open your board from your phone without a tunnel, port forwarding, or
|
|
98
|
+
ngrok.
|
|
99
|
+
|
|
100
|
+
Be clear about what this means. The relay is a reverse HTTP tunnel: a request
|
|
101
|
+
made from `app.galda.app` is forwarded over that socket, replayed against your
|
|
102
|
+
local server, and the response body is streamed back
|
|
103
|
+
(`engine/relay-client.mjs`).
|
|
104
|
+
|
|
105
|
+
**Therefore everything your board displays passes through Galda's relay
|
|
106
|
+
server:** task text, activity logs, review summaries, diffs, and proof
|
|
107
|
+
screenshots.
|
|
108
|
+
|
|
109
|
+
The connection is TLS-encrypted in transit. **It is not end-to-end encrypted**,
|
|
110
|
+
which means Galda's relay server is in the path and could read what crosses it.
|
|
111
|
+
We would rather write that down than let you assume otherwise. End-to-end
|
|
112
|
+
encryption is on the roadmap and is not implemented today.
|
|
113
|
+
|
|
114
|
+
**To turn the relay off entirely:**
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
RELAY_URL= npx @galda/cli
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
With no relay URL, the relay client is never started and no outbound socket is
|
|
121
|
+
opened (`bin/manager-for-ai.mjs`). You lose remote access from your phone.
|
|
122
|
+
Everything else works exactly the same.
|
|
123
|
+
|
|
124
|
+
### 3. Your Galda account — licensing
|
|
125
|
+
|
|
126
|
+
Galda checks your license. The verifying public key is compiled into the
|
|
127
|
+
package, so the check itself is offline, but the token is refreshed
|
|
128
|
+
periodically against the billing API. This carries your account identity, not
|
|
129
|
+
your code.
|
|
130
|
+
|
|
131
|
+
## Analytics
|
|
132
|
+
|
|
133
|
+
Galda records a small set of product events so we can see how the tool is
|
|
134
|
+
actually used and improve it. The list is exhaustive. If it is not named here,
|
|
135
|
+
we do not collect it.
|
|
136
|
+
|
|
137
|
+
**What we record:**
|
|
138
|
+
|
|
139
|
+
| Event | Purpose |
|
|
140
|
+
|---|---|
|
|
141
|
+
| `signup`, `signin` | Account lifecycle |
|
|
142
|
+
| `task_created`, `task_completed`, `task_failed`, `task_interrupted` | Completion and failure rates, so we can find what breaks |
|
|
143
|
+
| `review_approved`, `review_dismissed` | Whether results are actually usable |
|
|
144
|
+
| Which agent ran (Claude Code or Codex) | Where to spend engineering time |
|
|
145
|
+
| App version, OS, Node version | Compatibility and regression tracking |
|
|
146
|
+
| Crash and error events | Fixing crashes |
|
|
147
|
+
|
|
148
|
+
Each carries a timestamp and a pseudonymous account id, and nothing else.
|
|
149
|
+
|
|
150
|
+
**What we never record:**
|
|
151
|
+
|
|
152
|
+
- The text of your requests
|
|
153
|
+
- Your code, diffs, file contents, or file names
|
|
154
|
+
- Repository names or paths
|
|
155
|
+
- Proof screenshots and recordings
|
|
156
|
+
|
|
157
|
+
**Paid business plans: analytics are off.** On business plans no product events
|
|
158
|
+
are collected at all, the way Cursor forces Privacy Mode on for teams. You do
|
|
159
|
+
not have to configure anything.
|
|
160
|
+
|
|
161
|
+
**With the relay off, nothing is collected.** Not reduced, not anonymized:
|
|
162
|
+
nothing. A mode sold as "nothing leaves your machine" that quietly reported
|
|
163
|
+
usage would invalidate every other claim on this page, so it does not.
|
|
164
|
+
|
|
165
|
+
## Reporting a vulnerability
|
|
166
|
+
|
|
167
|
+
<!-- TODO(Masa): security contact address. -->
|
|
168
|
+
|
|
169
|
+
## 日本語
|
|
170
|
+
|
|
171
|
+
- Galda は AI モデルを内蔵せず、モデルと通信もしません。すでにあなたのマシンに
|
|
172
|
+
入っている Claude Code / Codex を起動するだけです。**あなたのコードが推論の
|
|
173
|
+
ために Galda へ送られることはありません。**
|
|
174
|
+
- **インストール時に何も実行しません**(`postinstall` 等が存在しません)。
|
|
175
|
+
- ローカルサーバは **`127.0.0.1` と `::1` にのみ bind** し、ネットワークからは
|
|
176
|
+
到達できません。アクセスキーが必須です。
|
|
177
|
+
- エージェントに渡している **git は読み取り専用**です。`commit` `push`
|
|
178
|
+
`checkout` `reset` を持ちません。任意のシェル実行もできません。
|
|
179
|
+
- 各タスクは**使い捨ての git worktree** で動き、あなたの作業ツリーには触れません。
|
|
180
|
+
- 既定では、スマホから使えるように **ボードの表示内容が `app.galda.app` の
|
|
181
|
+
relay を通ります**(TLS 保護、**E2E 暗号化は未実装**=経路上に Galda の
|
|
182
|
+
サーバがいます)。`RELAY_URL= npx @galda/cli` で relay を完全に切れます。
|
|
183
|
+
- アンインストールは `rm -rf ~/.manager-for-ai` だけです。
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Agent Manager — remote-access preference (the relay on/off switch)
|
|
2
|
+
//
|
|
3
|
+
// WHY THIS EXISTS: the relay is what lets you open your board from your phone,
|
|
4
|
+
// and it is on by default. But everything the board displays crosses Galda's
|
|
5
|
+
// relay server (see docs/SECURITY.md), so some people cannot or will not run
|
|
6
|
+
// it — a security policy that forbids outbound connections, or simply not
|
|
7
|
+
// wanting a third party in the path.
|
|
8
|
+
//
|
|
9
|
+
// That escape hatch already existed as `RELAY_URL= npx @galda/cli`, which is
|
|
10
|
+
// an env var nobody discovers. This makes it a setting, the way Cursor puts
|
|
11
|
+
// Privacy Mode in Settings rather than on the command line.
|
|
12
|
+
//
|
|
13
|
+
// The preference lives in a file in DATA_DIR because the two halves are
|
|
14
|
+
// SEPARATE PROCESSES: the server (:4400) takes the click, the launcher owns
|
|
15
|
+
// the relay child. Same idiom as relay-status.json / relay-takeover.req.
|
|
16
|
+
|
|
17
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
|
|
20
|
+
export const RELAY_PREF_FILE = 'relay-pref.json';
|
|
21
|
+
|
|
22
|
+
export function relayPrefPath(dataDir) {
|
|
23
|
+
return join(dataDir, RELAY_PREF_FILE);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Should the relay run? Pure — no clock, no I/O, no env.
|
|
27
|
+
//
|
|
28
|
+
// relayUrl : the configured relay endpoint ('' / undefined = none built in)
|
|
29
|
+
// pref : the user's saved choice (true | false | null when never set)
|
|
30
|
+
//
|
|
31
|
+
// Default is ON: a fresh install gets remote access, because that is the
|
|
32
|
+
// product. Only an explicit `false` turns it off, so a corrupt or missing
|
|
33
|
+
// preference file can never silently disable the feature.
|
|
34
|
+
export function resolveRelayEnabled({ relayUrl, pref } = {}) {
|
|
35
|
+
if (!relayUrl) return false;
|
|
36
|
+
return pref !== false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Read the saved preference. Returns null when the user has never chosen, so
|
|
40
|
+
// callers can tell "unset" from "explicitly on" — resolveRelayEnabled treats
|
|
41
|
+
// both as on, but the UI may want to say so differently.
|
|
42
|
+
export function readRelayPref(dataDir) {
|
|
43
|
+
try {
|
|
44
|
+
const raw = JSON.parse(readFileSync(relayPrefPath(dataDir), 'utf8'));
|
|
45
|
+
return typeof raw?.enabled === 'boolean' ? raw.enabled : null;
|
|
46
|
+
} catch {
|
|
47
|
+
return null; // never set, unreadable, or corrupt — all mean "no choice made"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Save the preference. Throws on a real write failure so the endpoint can
|
|
52
|
+
// report it rather than telling the user it saved something it did not.
|
|
53
|
+
export function writeRelayPref(dataDir, enabled) {
|
|
54
|
+
const value = enabled === true;
|
|
55
|
+
writeFileSync(relayPrefPath(dataDir), JSON.stringify({ enabled: value, at: Date.now() }) + '\n');
|
|
56
|
+
return value;
|
|
57
|
+
}
|
package/engine/server.mjs
CHANGED
|
@@ -24,6 +24,7 @@ import { needsProjectFolder, folderLabel, chooseFolderScript, parseChosenFolder,
|
|
|
24
24
|
import { parseStreamEvents, parseCodexEvents, buildCodexArgs, parsePlan, refinePlanTasks, canEditGoal, canDeleteGoal, shouldAskClarification, workerExitReason, classifyWorkerFailure, isForcedStop, taskStatusAfterVerify, workerResultText, taskCountsAsComplete, resolveWantsPR, resolveGoalSource, buildGoalSummary, buildReviewAttemptLedger, latestGoalOutcomeTask, buildDirtyWorkspacePrBlock, buildGoalProofMd, buildGoalPrBody, buildReviewSummaryPrompt, parseReviewSummary, buildReviewRuleSection, nextGoalStatus, isPrMerged, isPrApproved, reviewToDoneStatus, advanceReviewGoal, revertReviewGoal, approveGoal, dismissGoal, rejectGoal, reopenGoal, permissionModeFor, isPlanReview, nextAfterPlanApprove, GOAL_MODES, parseSkillFrontmatter, collectSkills, retestGoal, cancelRetestGoal, computeRetestOutcome, revertGoal, planRevertActions, archiveGoal, unarchiveGoal, undismissGoal, parseNumstat, truncateDiffText, sumUsage, resolveEntryUrl, rebasePreviewUrl, shouldCreateGoalPR, normalizePullRequestUrl, parseGitLog, diffNewCommits, replayQueueLog, reconcileOrphanGoals, reorderQueue, sortQueueByPriority, TASK_PRIORITIES, validateWorkflowColumns, DEFAULT_WORKFLOW_COLUMNS, validateReviewDefinition, DEFAULT_REVIEW_DEFINITION, resolveTestGate, buildEphemeralSeedLog, buildEphemeralSeedProjects, trimTaskActivityForState, buildAskContext, WORKER_TOOLS, workerPrompt, verifyCfAccessJwt, resolveIdentity, goalVisibleTo, clampParallelLimit, canStartMore, buildQueueWaits, nextRunnableTasks, goalsConflict, detectConflicts, pickFoldTarget, autoFoldReviewsEnabled, parseGitUnifiedDiffLocations, hasTestRelevantChanges, parseFailingTestNames, classifyTestGate, classifyPrSafety, isInconclusiveTestRun, countInfraFlakes, classifyRunFailures, classifyVerifyGate, buildVerificationInfraError, verificationInfraErrorFrom, buildExecutionPlan, buildContextHandoffSummary, buildFailurePostmortem, appendFailureMemory, latestFailurePolicy, classifyChangeRisk, checkRunBudget, usageBudgetTokens, isRateLimited, nextResumeDelay, checkFreeTierLimit, resolveEntitlement, resolveCachedEntitlement, FREE_TIER_LIMITS, QUEUED_GOAL_STATUSES, verifyLicenseToken, licenseTokenPayload, shouldRefreshLicense, shouldEmitSetupCompleted, pickAnalyticsUid, shouldEmitFreeExhausted, detectRequestLanguage, testFailureReason, manualTestRetryPrompt, isNothingVerifiable, isNothingVerifiableForPR, isSuspiciouslyIncompleteDone, classifyComposerIntentHeuristic, detectPauseIntent, buildIntentPrompt, parseIntentResponse, buildBoardSnapshot, validateBoardSnapshot, boardIsEmpty, decideBoardPull, resolveConnectedAgents, pickAvailableAgent, pickUtilityAgent, utilityModel, liveTakeoverDecision, shouldOpenBrowserOnBoot, goalTaskTitle, buildGoalTask, shouldUsePlannerForGoal, RUN_DECL_FILE, REQUIREMENT_EVIDENCE_FILE, parseRequirementEvidenceDocument, parseRunDeclaration, SHOT_DIR, collectShotNames, parseGoalAddress, REPLY_OUTCOMES, buildGoalMessage, serializeGoalMessage, parseGoalMessages, computeInternalQualityMetrics, classifyInternalQualityTaskError, resolveWorkspaceMode, goalSessionFor, setGoalAgentSession, switchGoalAgentSession, parseApprovalRequest } from './lib.mjs';
|
|
25
25
|
import { migrateRequirementModel, validateRequirementModel } from './requirement-model.mjs';
|
|
26
26
|
import { buildManagerVerificationChecks, buildRequirementVerificationEvidence, mergeRequirementVerificationEvidence } from './requirement-verification.mjs';
|
|
27
|
+
import { readRelayPref, writeRelayPref, resolveRelayEnabled } from './relay-pref.mjs';
|
|
27
28
|
import { createSerialQueue } from './lib.mjs';
|
|
28
29
|
import { wantsPullRequest } from './lib.mjs';
|
|
29
30
|
import { parseRequirementEvidence, unmappedChangedFiles, previewOpensSomethingElse } from './lib.mjs';
|
|
@@ -4436,6 +4437,41 @@ const server = createServer(async (req, res) => {
|
|
|
4436
4437
|
catch (e) { return json(res, 500, { error: String(e?.message ?? e) }); }
|
|
4437
4438
|
return json(res, 200, { ok: true });
|
|
4438
4439
|
}
|
|
4440
|
+
// GET/POST /api/relay-pref — the remote-access switch in Settings.
|
|
4441
|
+
// Remote access (the relay) is ON by default. Turning it off is the
|
|
4442
|
+
// discoverable form of `RELAY_URL= npx @galda/cli`: with it off no outbound
|
|
4443
|
+
// socket is opened and nothing your board displays crosses our server
|
|
4444
|
+
// (docs/SECURITY.md). The launcher owns the relay child in ANOTHER process,
|
|
4445
|
+
// so we persist the choice to a file it watches — same shape as
|
|
4446
|
+
// relay-takeover.req above.
|
|
4447
|
+
if (url.pathname === '/api/relay-pref' && req.method === 'GET') {
|
|
4448
|
+
const pref = readRelayPref(DATA_DIR);
|
|
4449
|
+
return json(res, 200, {
|
|
4450
|
+
pref,
|
|
4451
|
+
enabled: resolveRelayEnabled({ relayUrl: process.env.RELAY_URL, pref }),
|
|
4452
|
+
configured: Boolean(process.env.RELAY_URL),
|
|
4453
|
+
});
|
|
4454
|
+
}
|
|
4455
|
+
if (url.pathname === '/api/relay-pref' && req.method === 'POST') {
|
|
4456
|
+
let body = '';
|
|
4457
|
+
req.on('data', (d) => { body += d; });
|
|
4458
|
+
req.on('end', () => {
|
|
4459
|
+
let input;
|
|
4460
|
+
try { input = JSON.parse(body || '{}'); } catch { return json(res, 400, { error: 'bad json' }); }
|
|
4461
|
+
// A boolean and nothing else. "off"/0/undefined are how a security
|
|
4462
|
+
// setting silently becomes the opposite of what was clicked.
|
|
4463
|
+
if (typeof input?.enabled !== 'boolean') return json(res, 400, { error: 'enabled must be a boolean' });
|
|
4464
|
+
let saved;
|
|
4465
|
+
try { saved = writeRelayPref(DATA_DIR, input.enabled); }
|
|
4466
|
+
catch (e) { return json(res, 500, { error: String(e?.message ?? e) }); }
|
|
4467
|
+
return json(res, 200, {
|
|
4468
|
+
pref: saved,
|
|
4469
|
+
enabled: resolveRelayEnabled({ relayUrl: process.env.RELAY_URL, pref: saved }),
|
|
4470
|
+
configured: Boolean(process.env.RELAY_URL),
|
|
4471
|
+
});
|
|
4472
|
+
});
|
|
4473
|
+
return;
|
|
4474
|
+
}
|
|
4439
4475
|
// POST /api/signout — forget the local license so the settings panel can
|
|
4440
4476
|
// offer a plain "Log out" instead of requiring the `--signin` CLI flag to
|
|
4441
4477
|
// switch Google accounts. Clears the on-disk token and the in-memory cache;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@galda/cli",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.114",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Galda - hand off work to Claude Code or Codex, get proof back. Runs on your existing subscription, no extra API cost.",
|
|
6
6
|
"scripts": {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"puppeteer-core": "^23.11.1",
|
|
12
12
|
"ws": "^8.21.0"
|
|
13
13
|
},
|
|
14
|
-
"license": "
|
|
14
|
+
"license": "SEE LICENSE IN LICENSE.md",
|
|
15
15
|
"bin": {
|
|
16
16
|
"cli": "bin/manager-for-ai.mjs",
|
|
17
17
|
"galda": "bin/manager-for-ai.mjs",
|
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
"SKILL.md",
|
|
27
27
|
".mcp.json",
|
|
28
28
|
"README.md",
|
|
29
|
+
"LICENSE.md",
|
|
30
|
+
"docs/SECURITY.md",
|
|
29
31
|
"CLAUDE.md",
|
|
30
32
|
"!engine/chat-runs",
|
|
31
33
|
"!engine/test",
|
|
@@ -41,10 +43,6 @@
|
|
|
41
43
|
"engines": {
|
|
42
44
|
"node": ">=20"
|
|
43
45
|
},
|
|
44
|
-
"repository": {
|
|
45
|
-
"type": "git",
|
|
46
|
-
"url": "https://github.com/kodo-inc/agent-manager.git"
|
|
47
|
-
},
|
|
48
46
|
"keywords": [
|
|
49
47
|
"claude",
|
|
50
48
|
"claude-code",
|
|
@@ -53,5 +51,9 @@
|
|
|
53
51
|
"manager",
|
|
54
52
|
"verification",
|
|
55
53
|
"proof"
|
|
56
|
-
]
|
|
54
|
+
],
|
|
55
|
+
"homepage": "https://galda.app",
|
|
56
|
+
"bugs": {
|
|
57
|
+
"url": "https://galda.app/support"
|
|
58
|
+
}
|
|
57
59
|
}
|