@galda/cli 0.10.24 → 0.10.27
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 +12 -1
- package/app/index.html +137 -19
- package/engine/lib.mjs +40 -0
- package/engine/server.mjs +50 -2
- package/package.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -31,7 +31,18 @@
|
|
|
31
31
|
## 実装規律
|
|
32
32
|
|
|
33
33
|
- 変更は最小限。周辺コードの流儀に合わせる。
|
|
34
|
-
-
|
|
34
|
+
- **git は「行為」でなく「場所」で縛る**(Masa決定 2026-07-17・再議論しない)。
|
|
35
|
+
**commit=保存 / PR=主張 / merge=採用(Masaだけ)**。旧文言 `コミットしない` は**保存と主張を混ぜていた**上に、
|
|
36
|
+
worker には `WORKER_TOOLS`(`engine/lib.mjs`)に git が1文字も無く**権限で既に不可**=
|
|
37
|
+
**縛りたくない相手にだけ効き、PRを出すのにコミットが必須の CTO/engineer だけを縛っていた**ので削除。
|
|
38
|
+
- 🔴 **共有チェックアウト `~/agent-manager` で git を触らない**。ここは Masa の作業場所で、
|
|
39
|
+
全 worktree の親(実測135本)で、4401 の常駐 manager の起動元。HEAD が勝手に動く事故の出どころ
|
|
40
|
+
(2026-07-16 実発生)。`lane verify|pr|finish` が**ここで落ちる**。
|
|
41
|
+
- ✅ **自分のレーンの worktree(`~/am-wt-<topic>`)では、こまめにコミットする**。Masa の視界に入らない=
|
|
42
|
+
タダで、戻せる。Masa の言葉=「**あの時のあのバージョンがマジで失われて、もう一回作り直す**ことがある」。
|
|
43
|
+
「捨てる前提の3方向」も自分の worktree なら好きなだけコミットしてよい(嫌なのは**Masaの場所に置かれること**)。
|
|
44
|
+
- **Masa の視界に入るのは PR から**。`lane verify` は**汚れたツリーで落ち**、`lane pr` は**検証したコミット以外を
|
|
45
|
+
出荷させない**=「コミットせずに PR」「A を検証して B を出荷」は原理的に作れない。
|
|
35
46
|
- 報告は日本語で短く:何を変えたか・どのファイルか・人間が確認すべき点。
|
|
36
47
|
|
|
37
48
|
## 並行開発(Masa指示 2026-07-16・必須)
|
package/app/index.html
CHANGED
|
@@ -592,6 +592,10 @@
|
|
|
592
592
|
.task .name{font-size:13px;color:var(--ink);line-height:1.35;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}
|
|
593
593
|
.task.done .name{color:var(--ink)}
|
|
594
594
|
.task .cond{font-family:var(--mono);font-size:10.5px;color:var(--ink3);margin-top:3px}
|
|
595
|
+
/* PRD §6.5: a folded goal is settled, not failed — its title sinks to --ink2 like
|
|
596
|
+
any other settled row. No red, no painted surface, no badge (§1.5 art.8/art.9);
|
|
597
|
+
the pointer is the text itself, which reads as "the work is over there". */
|
|
598
|
+
.task.revrow.foldrow .name{color:var(--ink2)}
|
|
595
599
|
.replyctx{color:var(--ink2)}
|
|
596
600
|
.replyctx svg{width:11px;height:11px;vertical-align:-1px;stroke:currentColor;stroke-width:2;fill:none}
|
|
597
601
|
.side-foot{flex:0 0 auto;padding:11px 14px;border-top:1px solid var(--hair);font-size:11.5px;color:var(--ink3);display:flex;align-items:center;gap:7px}
|
|
@@ -1438,6 +1442,33 @@
|
|
|
1438
1442
|
/* review row number — #NN PREFIX on the title (HANDOFF-v45 §11), same form as the To Do
|
|
1439
1443
|
rows (PRD §5: numbering matches the sidebar) */
|
|
1440
1444
|
#fsRoot .rcard .rid{font-size:10.5px;font-weight:600;color:var(--ink3);margin:0 5px 0 0;display:inline-block;font-variant-numeric:tabular-nums}
|
|
1445
|
+
/* PRD §6.5 fold note — on the carrier ("also carries #13") and on the folded card
|
|
1446
|
+
("merged into #12"). Deliberately NOT the .pr treatment: no tinted ground, no
|
|
1447
|
+
badge (§1.5-9), no red — folding is not a failure. Mono + --ink3 = the same quiet
|
|
1448
|
+
meta tone the rest of the lane uses (§5.5「品は罫線とトーン差・monoで出す」). */
|
|
1449
|
+
/* display:block, not inline-block: it shares .rc with the PR pill, which is itself
|
|
1450
|
+
inline-block — side by side the pill sat on top of the last "#NN" and ate it. */
|
|
1451
|
+
#fsRoot .rcard .fnote{font-family:var(--mono);font-size:9.5px;color:var(--ink3);margin-top:3px;display:block}
|
|
1452
|
+
/* In rv3 the meta sits on the near-black photo scrim whatever the theme, where --ink3
|
|
1453
|
+
is effectively invisible (§5.5「見えるが騒がない」— near-invisible is a fail). Match
|
|
1454
|
+
.rid's white-on-scrim treatment. The folded card opts out of the scrim (rule above),
|
|
1455
|
+
so it keeps --ink3 on the panel where that tone is correct. */
|
|
1456
|
+
#fsRoot[data-rv="3"] .rcard:not(.fold) .fnote{color:rgba(255,255,255,.66);text-shadow:0 1px 3px rgba(0,0,0,.55)}
|
|
1457
|
+
/* A folded card is settled, not failed: title sinks to the settled tone, and it
|
|
1458
|
+
keeps no ✓/✕ because the thing to approve lives on the goal it was folded into. */
|
|
1459
|
+
#fsRoot .rcard.fold .t{color:var(--ink2)}
|
|
1460
|
+
/* A folded card is a pointer, not a review item: no proof to show, nothing to approve.
|
|
1461
|
+
So it opts OUT of the rv3 thumbnail treatment (whose .rc is absolutely positioned
|
|
1462
|
+
over an 84px photo ground) and stays the same quiet line in every view — the form
|
|
1463
|
+
Masa drew. Giving it a fake dark thumbnail would claim it has something to look at. */
|
|
1464
|
+
/* .fold.noimg, not .fold: the placeholder-ground rule below is `.rcard.noimg`, which
|
|
1465
|
+
ties this on specificity and would win on order — a folded card would keep the fake
|
|
1466
|
+
dark thumbnail ground. Matching both classes settles it regardless of order. */
|
|
1467
|
+
#fsRoot[data-rv="3"] .rcard.fold.noimg{display:flex;position:static;align-items:center;gap:10px;
|
|
1468
|
+
padding:9px 10px;min-height:0;border:1px solid var(--hair);border-radius:11px;background:none;box-shadow:none}
|
|
1469
|
+
#fsRoot[data-rv="3"] .rcard.fold.noimg .rc{position:static;padding:0;background:none}
|
|
1470
|
+
#fsRoot[data-rv="3"] .rcard.fold.noimg .t{color:var(--ink2);font-size:12.5px;text-shadow:none}
|
|
1471
|
+
#fsRoot[data-rv="3"] .rcard.fold.noimg .rid{color:var(--ink3)}
|
|
1441
1472
|
/* reference numbers everywhere else (HANDOFF-v45 §11): a leading #NN prefix on every
|
|
1442
1473
|
To Do item's name — same goal = same number (goalReviewNumber), distinct from the
|
|
1443
1474
|
PR's own #id shown separately. */
|
|
@@ -2545,11 +2576,18 @@ const STATUS_LABEL = {
|
|
|
2545
2576
|
// chip too; omitting it reproduces the original hardcoded labels. `startedAt`
|
|
2546
2577
|
// (goal.startedAt) disambiguates 'running': planning finishes with every
|
|
2547
2578
|
// task still 'queued', so it reads QUEUED until startedAt is set, then DOING.
|
|
2579
|
+
// Mirrors engine/lib.mjs goalChip() — this inline script can't import that
|
|
2580
|
+
// module. Had drifted out of sync (missing 'retesting'/'reverted'/'folded',
|
|
2581
|
+
// all silently falling to FAIL) until Masa caught a reverted goal reading as
|
|
2582
|
+
// FAIL (2026-07-17); keep this in lockstep with goalChip() by hand.
|
|
2548
2583
|
const GOAL_CHIP = (s, columns, startedAt) => s === 'stacked' || s === 'sending' ? [pickColumnLabel(columns, 'todo', 'QUEUED'), ''] :
|
|
2549
2584
|
s === 'running' && !startedAt ? [pickColumnLabel(columns, 'todo', 'QUEUED'), ''] :
|
|
2550
2585
|
['planning', 'running'].includes(s) ? [pickColumnLabel(columns, 'doing', 'DOING'), 'doing'] :
|
|
2551
2586
|
s === 'review' ? [pickColumnLabel(columns, 'review', 'REVIEW'), 'review'] :
|
|
2552
2587
|
s === 'done' ? [pickColumnLabel(columns, 'done', 'DONE'), ''] :
|
|
2588
|
+
s === 'retesting' ? ['RE-TESTING', 'doing'] :
|
|
2589
|
+
s === 'reverted' ? ['REVERTED', ''] :
|
|
2590
|
+
s === 'folded' ? ['FOLDED', ''] :
|
|
2553
2591
|
s === 'blocked' ? ['BLOCKED', 'blocked'] : s === 'needsInput' ? ['NEEDS INPUT', 'blocked'] : ['FAIL', ''];
|
|
2554
2592
|
const FINISHED = (s) => ['done', 'partial', 'failed', 'interrupted'].includes(s);
|
|
2555
2593
|
const ICONS = {
|
|
@@ -2703,6 +2741,16 @@ function reviewCardConfigFor(projectId){ return { ...DEFAULT_REVIEW_DEFINITION.r
|
|
|
2703
2741
|
// goal's own request language, same as the planner/worker; ja/en pin the
|
|
2704
2742
|
// project to one language regardless of what a given goal was typed in.
|
|
2705
2743
|
function reviewLangFor(projectId){ const l = reviewDefinitionFor(projectId).language; return (l === 'en' || l === 'ja') ? l : 'auto'; }
|
|
2744
|
+
// PRD §6.5 fold note language (Masa 2026-07-17: "ユーザーの言語に合わせて" — not
|
|
2745
|
+
// hardcoded English). Mirrors the server's own 'auto' resolution exactly
|
|
2746
|
+
// (reviewDefinition.language === 'auto' ? detectRequestLanguage(goal.text) : ...,
|
|
2747
|
+
// engine/server.mjs) so this note reads in the same language as the review
|
|
2748
|
+
// headline/PR body for the same goal: the project's fixed override wins if set,
|
|
2749
|
+
// otherwise match the language the goal's own request was written in.
|
|
2750
|
+
function foldLangFor(goal){
|
|
2751
|
+
const l = reviewLangFor(state.active);
|
|
2752
|
+
return (l === 'ja' || l === 'en') ? l : (/[-ヿ㐀-鿿]/.test(goal?.text || '') ? 'ja' : 'en');
|
|
2753
|
+
}
|
|
2706
2754
|
|
|
2707
2755
|
function esc(s){ const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; }
|
|
2708
2756
|
|
|
@@ -3609,7 +3657,16 @@ function renderTasks(){
|
|
|
3609
3657
|
// Feedback 3: the row carries only the check-line + PR link. Tests/proof are
|
|
3610
3658
|
// internal gate checks (they still block), NOT list clutter; the Review number
|
|
3611
3659
|
// is redundant with the title; tapping the row opens the detail (no button).
|
|
3660
|
+
// PRD §6.5: goals that landed on the same files were folded onto this one, so
|
|
3661
|
+
// they ride this PR instead of opening a second one editing the same lines.
|
|
3662
|
+
// Whoever opens this review must know it answers more than one request.
|
|
3663
|
+
// Only rendered when it actually happened (§1.5 art.3 — no empty container).
|
|
3664
|
+
const carried = pgoals.filter((x) => x.foldedInto === g.id);
|
|
3665
|
+
const carryNote = carried.length ? (foldLangFor(g) === 'ja'
|
|
3666
|
+
? `+ ${carried.map((x) => `#${x.id}`).join(', ')} の仕事もここに入っています`
|
|
3667
|
+
: `also carries ${carried.map((x) => `#${x.id}`).join(', ')}`) : '';
|
|
3612
3668
|
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>
|
|
3669
|
+
${carryNote ? `<div class="cond carries">${carryNote}</div>` : ''}
|
|
3613
3670
|
<div class="cond">${g.pr ? `<a class="prbig" href="${esc(g.pr)}" target="_blank">PR #${esc((g.pr.match(/pull\/(\d+)/) ?? [,''])[1])} ↗</a>` : ''}</div>
|
|
3614
3671
|
<div class="qreplyrow" data-qreplyrow="${g.id}"${state.qreplyOpen.has(g.id) ? '' : ' hidden'}><input type="text" data-qreplyinput="${g.id}" placeholder="直したい点を書いて送信(Todoに戻して再作業)"><button type="button" class="qreplysend" data-qreplysend="${g.id}" aria-label="Send">${ICONS.up}</button></div></div>
|
|
3615
3672
|
<div class="revquick">
|
|
@@ -3619,6 +3676,16 @@ function renderTasks(){
|
|
|
3619
3676
|
</div></div>`;
|
|
3620
3677
|
}).join('')
|
|
3621
3678
|
+ (reviewGoalsAll.length > REVIEW_LIMIT ? `<div class="more" data-reviewmore="1">${state.showAllReview ? 'show less' : `more · +${reviewGoalsAll.length - REVIEW_LIMIT}`}</div>` : '');
|
|
3679
|
+
// PRD §3.5: a folded goal is never deleted — deleting it just moves the question
|
|
3680
|
+
// from "why is my finished work stuck" to "where did #13 go". It keeps its row and
|
|
3681
|
+
// says where its work went. Not a failure: no red, no painted surface (§1.5 art.8);
|
|
3682
|
+
// no ✓/✕ either, because the thing to approve lives on the goal it was folded into.
|
|
3683
|
+
const foldedRows = pgoals.filter((g) => g.status === 'folded').map((g) => {
|
|
3684
|
+
const checkLine = (g.reviewSummary?.check || g.plan?.[0] || g.text || '').replace(/\s+/g, ' ').slice(0, 66);
|
|
3685
|
+
const mergedNote = foldLangFor(g) === 'ja' ? `→ #${esc(String(g.foldedInto))} にまとめました` : `merged into #${esc(String(g.foldedInto))}`;
|
|
3686
|
+
return `<div class="task revrow foldrow" data-goal="${g.id}"><div class="ic pending"></div><div class="tx"><div class="name">${esc(checkLine)} ${goalSourceBadge(g.source)}</div>
|
|
3687
|
+
<div class="cond">${mergedNote}</div></div></div>`;
|
|
3688
|
+
}).join('');
|
|
3622
3689
|
const doneRows = grouped(list.filter((t) => ['done', 'skipped'].includes(t.status)).sort((a, b) => b.num - a.num), (t) => row(t));
|
|
3623
3690
|
// Pending = deliberately shelved goals (e.g. "npm公開はPhase 2"): never
|
|
3624
3691
|
// planned or queued until Start is pressed. Parked below To do on purpose.
|
|
@@ -3630,7 +3697,7 @@ function renderTasks(){
|
|
|
3630
3697
|
const savedQAns = captureFocusedAnswerInput($('tasklist'), 'data-qansinput');
|
|
3631
3698
|
$('tasklist').innerHTML =
|
|
3632
3699
|
sec('Attention', attention, 'attn')
|
|
3633
|
-
+ sec(pickColumnLabel(cols, 'review', 'Review'), reviewDigest + review, 'reviewsec', reviewDefinitionFor(state.active).description, REVIEW_DEF_HINT)
|
|
3700
|
+
+ sec(pickColumnLabel(cols, 'review', 'Review'), reviewDigest + review + foldedRows, 'reviewsec', reviewDefinitionFor(state.active).description, REVIEW_DEF_HINT)
|
|
3634
3701
|
+ sec(pickColumnLabel(cols, 'doing', 'Doing'), doing)
|
|
3635
3702
|
+ sec(pickColumnLabel(cols, 'todo', 'To do'), todo, 'todosec')
|
|
3636
3703
|
+ sec('Pending', pendingRows, 'dim')
|
|
@@ -3673,9 +3740,16 @@ function renderTasks(){
|
|
|
3673
3740
|
|| e.target.closest('.revquick') || e.target.closest('.qreplyrow')) return;
|
|
3674
3741
|
const goalId = Number(el.dataset.goal);
|
|
3675
3742
|
const goal = state.goals.find((g) => g.id === goalId);
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3743
|
+
// A folded goal has nothing of its own to open — its work is in the goal it
|
|
3744
|
+
// was folded into. Send the click there. The target briefly goes back to
|
|
3745
|
+
// 'running' while it re-runs this goal's request, so resolve what to open
|
|
3746
|
+
// from the target's own status rather than assuming it is still in Review.
|
|
3747
|
+
const opened = goal?.status === 'folded' && goal.foldedInto != null
|
|
3748
|
+
? state.goals.find((x) => x.id === goal.foldedInto) ?? goal
|
|
3749
|
+
: goal;
|
|
3750
|
+
goToGoalInChat(opened.id);
|
|
3751
|
+
if (opened.status === 'review') openReviewChecklist(opened.id);
|
|
3752
|
+
else openGoalDetail(opened.id);
|
|
3679
3753
|
});
|
|
3680
3754
|
}
|
|
3681
3755
|
for (const b of $('tasklist').querySelectorAll('.reqopen')) {
|
|
@@ -5140,11 +5214,18 @@ function renderFsReview(){
|
|
|
5140
5214
|
const pgoals = state.goals.filter((g) => g.projectId === state.active);
|
|
5141
5215
|
const list = state.tasks.filter((t) => t.projectId === state.active);
|
|
5142
5216
|
const rows = buildReviewDigest({ goals: pgoals, tasks: list });
|
|
5143
|
-
|
|
5217
|
+
// PRD §3.5/§6.5: folded goals still have a row — they were never deleted, their work
|
|
5218
|
+
// just went somewhere else. They must be able to hold this section up ALONE: the goal
|
|
5219
|
+
// they were folded into flips back to 'running' while it re-runs their request, so
|
|
5220
|
+
// there is a window where `rows` is empty and only the folded ones remain. Returning
|
|
5221
|
+
// early there would make #13 vanish for the length of the re-run — exactly the "where
|
|
5222
|
+
// did my finished work go" this feature exists to prevent.
|
|
5223
|
+
const foldedGoals = pgoals.filter((g) => g.status === 'folded');
|
|
5224
|
+
if (!rows.length && !foldedGoals.length) { el.innerHTML = ''; return; } // §1.5-3: no reviews → the UI itself does not exist
|
|
5144
5225
|
const green = rows.filter((r) => !r.testResult || r.testResult.ok !== false).length;
|
|
5145
5226
|
const proofN = rows.filter((r) => r.proof).length;
|
|
5146
5227
|
const blockedN = pgoals.filter((g) => g.status === 'blocked').length;
|
|
5147
|
-
const pct = Math.round((green / rows.length) * 100);
|
|
5228
|
+
const pct = rows.length ? Math.round((green / rows.length) * 100) : 0;
|
|
5148
5229
|
// CDO review 2026-07-08 §6: same fix as the s5sum digest above — green/rows.length
|
|
5149
5230
|
// already exclude blocked goals (different status, never enters `rows`), so the
|
|
5150
5231
|
// ratio was already correct; "all green" is the line that used to fire regardless
|
|
@@ -5170,26 +5251,49 @@ function renderFsReview(){
|
|
|
5170
5251
|
// retest ×3 note (v45 §5.3): the goal came back green from a Ledger Dismiss
|
|
5171
5252
|
const g0 = pgoals.find((g) => g.id === r.goalIds[0]);
|
|
5172
5253
|
const rnote = g0?.retest?.passed ? `<span class="rnote">✓ ${esc(g0.retest.note)}</span>` : '';
|
|
5173
|
-
|
|
5254
|
+
// PRD §6.5: goals that landed on the same files ride this card's PR instead of
|
|
5255
|
+
// opening a second one that edits the same lines. Whoever opens this review has
|
|
5256
|
+
// to know it answers more than one request. Only when it happened (§1.5-3).
|
|
5257
|
+
const carried = pgoals.filter((x) => r.goalIds.includes(x.foldedInto));
|
|
5258
|
+
const cnote = carried.length ? `<span class="fnote">${foldLangFor(g0) === 'ja'
|
|
5259
|
+
? `+ ${carried.map((x) => `#${x.id}`).join(', ')} の仕事もここに入っています`
|
|
5260
|
+
: `also carries ${carried.map((x) => `#${x.id}`).join(', ')}`}</span>` : '';
|
|
5261
|
+
return `<div class="rcard${img ? '' : ' noimg'}" data-fsrev="${r.goalIds[0]}">${img}<div class="rc"><div class="t">${rid}${esc(r.checkLine)}</div>${rnote}${cnote}${pr}</div>
|
|
5174
5262
|
<div class="act"><button class="b" data-fsapprove="${ids}" title="Approve">✓</button><button class="b no" data-fsdismiss="${ids}" title="Send back">✕</button></div></div>`;
|
|
5175
5263
|
};
|
|
5264
|
+
// A folded goal is settled, not failed: no ✓/✕ (the thing to approve lives on the goal
|
|
5265
|
+
// it was folded into), no red, no painted surface (§1.5-8) — just its own number, what
|
|
5266
|
+
// it asked for, and where the work went. Tapping it opens that goal (§1.5-9 wording).
|
|
5267
|
+
const foldCard = (g) => {
|
|
5268
|
+
const line = String(g.reviewSummary?.check || g.plan?.[0] || g.text || '').replace(/\s+/g, ' ').slice(0, 60);
|
|
5269
|
+
const mergedNote = foldLangFor(g) === 'ja' ? `→ #${esc(String(g.foldedInto))} にまとめました` : `merged into #${esc(String(g.foldedInto))}`;
|
|
5270
|
+
return `<div class="rcard noimg fold" data-fsfold="${g.id}">
|
|
5271
|
+
<div class="rc"><div class="t"><span class="rid gno" data-n="${g.id}">#${g.id}</span>${esc(line)}</div>
|
|
5272
|
+
<span class="fnote">${mergedNote}</span></div></div>`;
|
|
5273
|
+
};
|
|
5176
5274
|
// V3-C (Masa決定 2026-07-07): no collapsed strip — it duplicated the sm2 meter right
|
|
5177
5275
|
// above. Reviews render directly: meter + Review header + cards (+N more fold, §1.5-8).
|
|
5178
|
-
|
|
5276
|
+
// The meter counts what is reviewable, so it only exists when something is (§1.5-3):
|
|
5277
|
+
// during the re-run window the section can be folded-rows-only, and "0 / 0 ready" is
|
|
5278
|
+
// a claim about nothing.
|
|
5279
|
+
el.innerHTML = `${rows.length ? `<div class="sm2">
|
|
5179
5280
|
<div class="m-top"><span class="num mt">${green} / ${rows.length}</span><span> ready</span><button class="detailbtn" id="fsSeeAll">See all</button></div>
|
|
5180
5281
|
<div class="m-bar"><i style="width:${pct}%"></i></div>
|
|
5181
|
-
<div class="s">${esc(sline)}</div></div
|
|
5282
|
+
<div class="s">${esc(sline)}</div></div>` : ''}
|
|
5182
5283
|
<div class="revhd" id="fsRevHd"><span class="hd">Review</span><span class="rnum num">${green} ready</span>
|
|
5183
5284
|
<span class="rvbtns">
|
|
5184
5285
|
<button class="rvb${fsUI.rv === 3 ? ' on' : ''}" data-fsrv="3" title="Thumbnails"><svg viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 15l5-4 4 3 4-5 5 6"/></svg></button>
|
|
5185
5286
|
<button class="rvb${fsUI.rv === 1 ? ' on' : ''}" data-fsrv="1" title="List"><svg viewBox="0 0 24 24"><rect x="3" y="4" width="7" height="7" rx="1.5"/><path d="M13 6h8M13 9h5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><path d="M13 16h8M13 19h5"/></svg></button>
|
|
5186
5287
|
<button class="rvb${fsUI.rv === 2 ? ' on' : ''}" data-fsrv="2" title="Compact"><svg viewBox="0 0 24 24"><path d="M3 6h18M3 12h18M3 18h18"/></svg></button>
|
|
5187
5288
|
</span></div>
|
|
5188
|
-
<div class="revlist">${shown.map(card).join('')}
|
|
5289
|
+
<div class="revlist">${shown.map(card).join('')}${foldedGoals.map(foldCard).join('')}
|
|
5189
5290
|
${rows.length > RCAP ? `<div class="morefold" id="fsRevMore">${fsUI.revMore ? 'less' : `+${rows.length - RCAP} more`}<span class="g">›</span></div>` : ''}</div>`;
|
|
5190
5291
|
// See all = the Ledger dialog (HANDOFF-v45 §5, replacing the ペライチ);
|
|
5191
5292
|
// Cmd/Ctrl+click = standalone full-page view in a new tab (#seeall route).
|
|
5192
|
-
|
|
5293
|
+
// guarded: the meter (and with it See all) is absent while the section holds only
|
|
5294
|
+
// folded rows — see the §1.5-3 note above.
|
|
5295
|
+
const seeAll = $('fsSeeAll');
|
|
5296
|
+
if (seeAll) seeAll.onclick = (e) => { e.stopPropagation();
|
|
5193
5297
|
if (e.metaKey || e.ctrlKey) { window.open(saStandaloneUrl(), '_blank'); return; }
|
|
5194
5298
|
saOpen(); };
|
|
5195
5299
|
const moreEl = el.querySelector('#fsRevMore');
|
|
@@ -5200,6 +5304,19 @@ function renderFsReview(){
|
|
|
5200
5304
|
if (e.target.closest('a') || e.target.closest('.act')) return;
|
|
5201
5305
|
openReviewChecklist(Number(c.dataset.fsrev));
|
|
5202
5306
|
};
|
|
5307
|
+
// A folded goal has nothing of its own to review — its work is in the goal it was
|
|
5308
|
+
// folded into, so the tap goes there. That goal is only in Review once its re-run
|
|
5309
|
+
// finishes, so resolve from its live status instead of assuming (openReviewChecklist
|
|
5310
|
+
// on a running goal would present an empty checklist).
|
|
5311
|
+
for (const c of el.querySelectorAll('[data-fsfold]')) c.onclick = (e) => {
|
|
5312
|
+
if (e.target.closest('a')) return;
|
|
5313
|
+
const g = state.goals.find((x) => x.id === Number(c.dataset.fsfold));
|
|
5314
|
+
const target = state.goals.find((x) => x.id === g?.foldedInto);
|
|
5315
|
+
if (!target) return;
|
|
5316
|
+
goToGoalInChat(target.id);
|
|
5317
|
+
if (target.status === 'review') openReviewChecklist(target.id);
|
|
5318
|
+
else openGoalDetail(target.id);
|
|
5319
|
+
};
|
|
5203
5320
|
for (const b of el.querySelectorAll('[data-fsapprove]')) b.onclick = async (e) => { e.stopPropagation();
|
|
5204
5321
|
let r; for (const id of b.dataset.fsapprove.split(',')) r = await fetch(withKey(`/api/goals/${id}/approve`), { method: 'POST' });
|
|
5205
5322
|
if (r && r.ok) { celebrateApprove(); await refresh(); } else showErr('Approve failed.'); };
|
|
@@ -6061,12 +6178,13 @@ function bootDisconnected(info){
|
|
|
6061
6178
|
#connectGate .cg-switch a{color:var(--blue,#8fb0ff);text-decoration:none}
|
|
6062
6179
|
/* A long wait (15s+) almost always means an account mismatch, not a slow
|
|
6063
6180
|
first connect (Masa 2026-07-17: kept re-running npx, kept landing back
|
|
6064
|
-
here — the always-on caption below was too easy to miss).
|
|
6065
|
-
the
|
|
6066
|
-
|
|
6067
|
-
|
|
6068
|
-
|
|
6069
|
-
#connectGate .cg-switch.escalated
|
|
6181
|
+
here — the always-on caption below was too easy to miss). At this point
|
|
6182
|
+
the board is unusable until the user acts, so this reads as an error
|
|
6183
|
+
state, not a caution — --danger, same tone as failed steps/review chips
|
|
6184
|
+
(Masa 2026-07-17: escalated hint should be red, not amber) — color only,
|
|
6185
|
+
no filled box, per the hover/attention convention used everywhere else. */
|
|
6186
|
+
#connectGate .cg-switch.escalated{color:var(--danger);font-weight:600}
|
|
6187
|
+
#connectGate .cg-switch.escalated a{color:var(--danger);text-decoration:underline}
|
|
6070
6188
|
#connectGate .cg-x{position:absolute;top:14px;right:16px;width:28px;height:28px;border:0;background:transparent;color:var(--ink3,#6b6862);font-size:20px;line-height:1;cursor:pointer}
|
|
6071
6189
|
#connectGate .cg-wait{display:inline-flex;align-items:center;gap:7px;font:500 10.5px/1 var(--mono,monospace);letter-spacing:.08em;color:var(--ink3,#6b6862);margin-top:16px}
|
|
6072
6190
|
#connectGate .cg-wait i{width:6px;height:6px;border-radius:50%;background:var(--green,#3DDC97);animation:cgpulse 1.8s ease-in-out infinite}
|
|
@@ -6088,7 +6206,7 @@ function bootDisconnected(info){
|
|
|
6088
6206
|
+ '<a href="https://openai.com/codex/" target="_blank" rel="noreferrer">Codex</a>, then run the same command '
|
|
6089
6207
|
+ '— Galda uses whichever agent you have.</p>'
|
|
6090
6208
|
+ '<p class="cg-who">Signed in as <b></b></p>'
|
|
6091
|
-
+ '<p class="cg-switch">Already running Galda under a different account? Run <code>npx @galda/cli --signin</code> and pick this one. Or <a href="/logout">switch
|
|
6209
|
+
+ '<p class="cg-switch">Already running Galda under a different account? Run <code>npx @galda/cli --signin</code> and pick this one. Or <a href="/logout">switch Google account →</a></p>'
|
|
6092
6210
|
+ '<div class="cg-wait"><i></i>WAITING FOR THIS DEVICE</div></div>';
|
|
6093
6211
|
document.body.appendChild(ov);
|
|
6094
6212
|
ov.querySelector('.cg-who b').textContent = email || 'your account';
|
|
@@ -6131,7 +6249,7 @@ function bootDisconnected(info){
|
|
|
6131
6249
|
const CG_ESCALATE_MS = 15000;
|
|
6132
6250
|
const escalateTimer = (email && cgSwitch) ? setTimeout(() => {
|
|
6133
6251
|
cgSwitch.classList.add('escalated');
|
|
6134
|
-
cgSwitch.innerHTML = `Still waiting? Galda may already be running on this computer under a <b>different account</b> than <b>${email.replace(/[<>&]/g, (c) => ({ '<': '<', '>': '>', '&': '&' }[c]))}</b>. Run <code>npx @galda/cli --signin</code> in that terminal and pick this account. Or <a href="/logout">switch
|
|
6252
|
+
cgSwitch.innerHTML = `Still waiting? Galda may already be running on this computer under a <b>different account</b> than <b>${email.replace(/[<>&]/g, (c) => ({ '<': '<', '>': '>', '&': '&' }[c]))}</b>. Run <code>npx @galda/cli --signin</code> in that terminal and pick this account. Or <a href="/logout">switch Google account →</a>`;
|
|
6135
6253
|
}, CG_ESCALATE_MS) : null;
|
|
6136
6254
|
try {
|
|
6137
6255
|
const es = new EventSource('/presence');
|
package/engine/lib.mjs
CHANGED
|
@@ -1069,6 +1069,7 @@ export function goalGroupStatus(goalStatus, startedAt) {
|
|
|
1069
1069
|
if (goalStatus === 'planning' || goalStatus === 'running') return 'doing';
|
|
1070
1070
|
if (goalStatus === 'review') return 'review';
|
|
1071
1071
|
if (goalStatus === 'retesting') return 'doing'; // Ledger Dismiss → engine re-testing ×3 (v45 §5.3)
|
|
1072
|
+
if (goalStatus === 'folded') return 'review'; // PRD §6.5: rendered inside the Review section, not Done
|
|
1072
1073
|
if (goalStatus === 'blocked') return 'attn'; // §4E: blocked is Attention, not Done
|
|
1073
1074
|
if (goalStatus === 'needsInput') return 'todo'; // smart intake: awaiting an answer
|
|
1074
1075
|
return 'done'; // includes 'reverted' (terminal)
|
|
@@ -1101,6 +1102,7 @@ export function goalChip(goalStatus, columns, startedAt) {
|
|
|
1101
1102
|
if (goalStatus === 'done') return [pickColumnLabel(columns, 'done', 'DONE'), ''];
|
|
1102
1103
|
if (goalStatus === 'retesting') return ['RE-TESTING', 'doing']; // Ledger Dismiss → retest ×3 (v45 §5.3)
|
|
1103
1104
|
if (goalStatus === 'reverted') return ['REVERTED', '']; // Ledger Revert: terminal, distinct from archived
|
|
1105
|
+
if (goalStatus === 'folded') return ['FOLDED', '']; // PRD §6.5: settled, not failed — no red/badge class, matches 'done'/'reverted'
|
|
1104
1106
|
if (goalStatus === 'blocked') return ['BLOCKED', 'blocked']; // §4E: distinct state (reason + reverify), not FAIL/Done
|
|
1105
1107
|
if (goalStatus === 'needsInput') return ['確認待ち', 'blocked']; // smart intake: awaiting an answer
|
|
1106
1108
|
return ['FAIL', ''];
|
|
@@ -1595,6 +1597,44 @@ export function detectConflicts(goal, otherGoals) {
|
|
|
1595
1597
|
.map((g) => g.id);
|
|
1596
1598
|
}
|
|
1597
1599
|
|
|
1600
|
+
// ---- the review gate (PRD §6.5) --------------------------------------------
|
|
1601
|
+
// Detection alone was never the point: a flag that says "this conflicts with
|
|
1602
|
+
// #38" still leaves the human to untangle two PRs that edit the same lines.
|
|
1603
|
+
// PRD §6.5 requires that PRs sitting in Review never conflict WITH EACH OTHER,
|
|
1604
|
+
// so work that lands on the same paths must land on the SAME PR.
|
|
1605
|
+
//
|
|
1606
|
+
// This is the last of the three gates (queue / doing / review) and the only one
|
|
1607
|
+
// that knows the truth: by the time a goal finishes, its changed files are
|
|
1608
|
+
// measured, not guessed. So the promise rests here, and the earlier gates are
|
|
1609
|
+
// free to be wrong — being wrong costs a re-run, never a conflict.
|
|
1610
|
+
//
|
|
1611
|
+
// Why the caller re-runs the folded goal's intent inside the target's worktree
|
|
1612
|
+
// instead of replaying its diff: engine/pr.mjs COPIES changed files into the PR
|
|
1613
|
+
// worktree (cp -R) rather than merging them, so aiming this goal's PR at the
|
|
1614
|
+
// target's branch would silently overwrite the target's version of any shared
|
|
1615
|
+
// file. Re-running lets the worker SEE the target's changes and build on them,
|
|
1616
|
+
// which means there is never a conflict to resolve in the first place.
|
|
1617
|
+
//
|
|
1618
|
+
// `candidates` are same-project goals already awaiting review (each
|
|
1619
|
+
// `{ id, changedFiles }`). Returns the id to fold into, or null to open a
|
|
1620
|
+
// normal PR.
|
|
1621
|
+
//
|
|
1622
|
+
// Deliberately folds ONLY when exactly one review goal overlaps. With two or
|
|
1623
|
+
// more, folding is not merely insufficient, it is harmful: pulling goal B into
|
|
1624
|
+
// A's PR drags B's paths along, so A's PR starts colliding with C's where it
|
|
1625
|
+
// did not before. That cluster wants one shared PR, which we cannot build by
|
|
1626
|
+
// bolting onto an already-open one — so we leave those flagged (the old
|
|
1627
|
+
// behaviour) rather than make the pile worse. Known remainder, named in the
|
|
1628
|
+
// handoff, not silently swallowed.
|
|
1629
|
+
export function pickFoldTarget(goal, candidates) {
|
|
1630
|
+
const files = goal?.changedFiles ?? [];
|
|
1631
|
+
if (!files.length) return null;
|
|
1632
|
+
const overlapping = (candidates ?? []).filter(
|
|
1633
|
+
(g) => g && g.id !== goal.id && goalsConflict(files, g.changedFiles),
|
|
1634
|
+
);
|
|
1635
|
+
return overlapping.length === 1 ? overlapping[0].id : null;
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1598
1638
|
// Which of `commits` (newest-first, as returned by parseGitLog) are new
|
|
1599
1639
|
// since we last synced up to `lastSeenHash`? null means nothing has been
|
|
1600
1640
|
// seen yet, so every commit is new. If lastSeenHash isn't found in the
|
package/engine/server.mjs
CHANGED
|
@@ -21,7 +21,7 @@ import { homedir } from 'node:os';
|
|
|
21
21
|
import { fileURLToPath } from 'node:url';
|
|
22
22
|
import { pathToFileURL } from 'node:url';
|
|
23
23
|
import { needsProjectFolder, folderLabel, buildFolderQuestion, resolveFolderAnswer, classifyFolderTarget, buildFolderInitConfirm, isFolderInitConfirmed, needsReviewPreference, buildReviewPreferenceQuestion, resolveReviewPreferenceAnswer, routeQuestionAnswer, notUnderstoodNote, parseRelocalizedQuestion, classifyAuthProbe, authBannerText } from './lib.mjs';
|
|
24
|
-
import { parseStreamEvents, parsePlan, refinePlanTasks, findOverlappingGoal, canEditGoal, canDeleteGoal, shouldAskClarification, workerExitReason, isForcedStop, workerResultText, taskCountsAsComplete, resolveWantsPR, resolveGoalSource, buildGoalSummary, buildGoalProofMd, buildGoalPrBody, buildReviewRuleSection, nextGoalStatus, isPrMerged, isPrApproved, reviewToDoneStatus, advanceReviewGoal, revertReviewGoal, approveGoal, dismissGoal, 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, verifyCfAccessJwt, resolveIdentity, goalVisibleTo, clampParallelLimit, canStartMore, nextRunnableTasks, goalsConflict, detectConflicts, isUiChange, shouldCaptureProof, shouldCaptureBaseline, hasTestRelevantChanges, buildExecutionPlan, buildContextHandoffSummary, buildFailurePostmortem, appendFailureMemory, latestFailurePolicy, classifyChangeRisk, checkRunBudget, usageBudgetTokens, isRateLimited, nextResumeDelay, checkFreeTierLimit, resolveEntitlement, resolveCachedEntitlement, FREE_TIER_LIMITS, verifyLicenseToken, shouldEmitSetupCompleted, pickAnalyticsUid, shouldEmitFreeExhausted, detectRequestLanguage, isNothingVerifiable, classifyComposerIntentHeuristic, buildIntentPrompt, parseIntentResponse, buildBoardSnapshot, validateBoardSnapshot, boardIsEmpty, decideBoardPull, resolveConnectedAgents, pickAvailableAgent, pickUtilityAgent, utilityModel, liveTakeoverDecision } from './lib.mjs';
|
|
24
|
+
import { parseStreamEvents, parsePlan, refinePlanTasks, findOverlappingGoal, canEditGoal, canDeleteGoal, shouldAskClarification, workerExitReason, isForcedStop, workerResultText, taskCountsAsComplete, resolveWantsPR, resolveGoalSource, buildGoalSummary, buildGoalProofMd, buildGoalPrBody, buildReviewRuleSection, nextGoalStatus, isPrMerged, isPrApproved, reviewToDoneStatus, advanceReviewGoal, revertReviewGoal, approveGoal, dismissGoal, 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, verifyCfAccessJwt, resolveIdentity, goalVisibleTo, clampParallelLimit, canStartMore, nextRunnableTasks, goalsConflict, detectConflicts, pickFoldTarget, isUiChange, shouldCaptureProof, shouldCaptureBaseline, hasTestRelevantChanges, buildExecutionPlan, buildContextHandoffSummary, buildFailurePostmortem, appendFailureMemory, latestFailurePolicy, classifyChangeRisk, checkRunBudget, usageBudgetTokens, isRateLimited, nextResumeDelay, checkFreeTierLimit, resolveEntitlement, resolveCachedEntitlement, FREE_TIER_LIMITS, verifyLicenseToken, shouldEmitSetupCompleted, pickAnalyticsUid, shouldEmitFreeExhausted, detectRequestLanguage, isNothingVerifiable, classifyComposerIntentHeuristic, buildIntentPrompt, parseIntentResponse, buildBoardSnapshot, validateBoardSnapshot, boardIsEmpty, decideBoardPull, resolveConnectedAgents, pickAvailableAgent, pickUtilityAgent, utilityModel, liveTakeoverDecision } from './lib.mjs';
|
|
25
25
|
import { openPR } from './pr.mjs';
|
|
26
26
|
import { runVerification, exerciseUi } from './verify.mjs';
|
|
27
27
|
|
|
@@ -2478,7 +2478,55 @@ async function finishGoalIfComplete(goal, project, activityTask = null) {
|
|
|
2478
2478
|
// network needed) — this goal's changed files are already final at this
|
|
2479
2479
|
// point (all its tasks are done), so its conflictsWith is accurate even if
|
|
2480
2480
|
// PR creation itself fails (e.g. no `gh` auth) or is still no-op'd (no code).
|
|
2481
|
-
updateGoalConflicts(goal);
|
|
2481
|
+
const conflicts = updateGoalConflicts(goal);
|
|
2482
|
+
// PRD §6.5 — the review gate. conflictsWith above only DESCRIBES the
|
|
2483
|
+
// collision; this acts on it. A goal that landed on the same paths as one
|
|
2484
|
+
// already awaiting review must not open a SECOND PR: both would edit the same
|
|
2485
|
+
// lines and fight when they land, which is exactly the "10 goals, 10
|
|
2486
|
+
// conflicting PRs" morning §6.5 exists to kill. Same-place work goes onto the
|
|
2487
|
+
// same PR, so what sits in Review never conflicts with itself.
|
|
2488
|
+
//
|
|
2489
|
+
// This is the last of the three gates (queue / doing / review) and the only
|
|
2490
|
+
// one that carries the promise: the files compared here are measured, not
|
|
2491
|
+
// predicted. That is what lets the earlier, cheaper gates guess — a wrong
|
|
2492
|
+
// guess costs the re-run below, never a conflict.
|
|
2493
|
+
//
|
|
2494
|
+
// We re-run this goal's intent inside the target's worktree rather than
|
|
2495
|
+
// replaying its diff, because openPR COPIES changed files into the PR
|
|
2496
|
+
// worktree instead of merging them: aiming this goal at the target's branch
|
|
2497
|
+
// would silently overwrite the target's version of any shared file. Re-running
|
|
2498
|
+
// lets the worker SEE the target's changes and build on them, so there is
|
|
2499
|
+
// never a conflict to resolve. It costs one extra worker pass — the price of
|
|
2500
|
+
// gates 1/2 having missed, paid in tokens instead of in the user's morning.
|
|
2501
|
+
//
|
|
2502
|
+
// Same owner only: folding across owners would splice one user's work onto
|
|
2503
|
+
// another's PR and echo its text back — the same leak the intake-side fold
|
|
2504
|
+
// guards against with a visibility check.
|
|
2505
|
+
const foldTarget = conflicts.length
|
|
2506
|
+
? goals.find((g) => g.id === pickFoldTarget(
|
|
2507
|
+
{ id: goal.id, changedFiles: goalChangedFileUnion(goal.id) },
|
|
2508
|
+
goals
|
|
2509
|
+
.filter((c) => c.id !== goal.id
|
|
2510
|
+
&& c.projectId === goal.projectId
|
|
2511
|
+
&& c.status === 'review'
|
|
2512
|
+
&& (c.owner ?? 'owner') === (goal.owner ?? 'owner'))
|
|
2513
|
+
.map((c) => ({ id: c.id, changedFiles: goalChangedFileUnion(c.id) })),
|
|
2514
|
+
))
|
|
2515
|
+
: null;
|
|
2516
|
+
if (foldTarget) {
|
|
2517
|
+
goalAct(`Same files as goal ${foldTarget.id} — folding onto its PR instead of opening a second one.`);
|
|
2518
|
+
// The target re-opens: it has new work to do, and createGoalPR's rework path
|
|
2519
|
+
// will push the result onto the PR branch it already owns.
|
|
2520
|
+
foldTarget.status = 'running';
|
|
2521
|
+
queueReplyTask(foldTarget, goal.text.slice(0, 8000));
|
|
2522
|
+
saveGoal(foldTarget);
|
|
2523
|
+
// The folded goal keeps its row (PRD §3.5: a submission is never dropped) —
|
|
2524
|
+
// it becomes a pointer at the goal that now carries its work.
|
|
2525
|
+
goal.status = 'folded';
|
|
2526
|
+
goal.foldedInto = foldTarget.id;
|
|
2527
|
+
saveGoal(goal);
|
|
2528
|
+
return;
|
|
2529
|
+
}
|
|
2482
2530
|
// Computed BEFORE createGoalPR (not inside verifyGate, which used to run
|
|
2483
2531
|
// after) so the PR body's "確認ポイント" headline is available the first
|
|
2484
2532
|
// time the PR is opened, not only on the review card (Masa 2026-07-16).
|
package/package.json
CHANGED