@galda/cli 0.10.103 → 0.10.105
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/index.html +109 -40
- package/app/review-merge-celebration-preview.html +81 -6
- package/engine/lib.mjs +22 -8
- package/engine/server.mjs +11 -4
- package/package.json +1 -1
package/app/index.html
CHANGED
|
@@ -613,6 +613,9 @@
|
|
|
613
613
|
.qreplysend{width:30px;height:30px;flex:0 0 30px;display:grid;place-items:center;border:0;border-radius:50%;background:var(--iri);color:var(--on-accent);cursor:pointer;padding:0}
|
|
614
614
|
.qreplysend svg{width:15px;height:15px;stroke:currentColor;stroke-width:2;fill:none}
|
|
615
615
|
.prbig{font-family:var(--mono);font-size:10.5px;color:var(--green);text-decoration:none;border:1px solid rgba(var(--green-rgb),.4);background:rgba(var(--green-rgb),.12);border-radius:7px;padding:3px 10px;font-weight:600}
|
|
616
|
+
/* A PR closed on GitHub without merging is not a success — mute it off the green
|
|
617
|
+
"open" treatment (Masa: closed should read as closed, not celebrated). */
|
|
618
|
+
.prbig.closed,.ppr.closed,a.sa-delivery.closed{color:var(--ink2)!important;border-color:var(--hair2);background:transparent}
|
|
616
619
|
/* task 228: レビューダイジェスト — Reviewセクション最上部の一枚まとめ。行=確認1行
|
|
617
620
|
+proofサムネ(小)+テストバッジ+PRリンク+その場でApprove/Reject。下の個別行(revrow)
|
|
618
621
|
はそのまま詳細一覧として残す。 */
|
|
@@ -2232,6 +2235,19 @@
|
|
|
2232
2235
|
/* Changed/Tests aligns with the request TEXT, not the number/chevron — same 18px
|
|
2233
2236
|
the fold body below uses, so both sit under the same column (Masa 2026-07-29). */
|
|
2234
2237
|
.redesign .rqit .rqev{font-size:12px;color:var(--ink3);margin:5px 0 0;padding-left:18px}
|
|
2238
|
+
/* Canonical requirement coverage: one compact ledger, not three competing
|
|
2239
|
+
interpretations. Status is text + a small rule/mark (no badge surface),
|
|
2240
|
+
and Manager checks stay visually distinct from worker claims. */
|
|
2241
|
+
.redesign .rqcov{display:flex;flex-direction:column;margin-top:2px}
|
|
2242
|
+
.redesign .rqcov-row{padding:11px 0;border-top:1px solid var(--hair)}
|
|
2243
|
+
.redesign .rqcov-row:first-child{padding-top:0;border-top:0}
|
|
2244
|
+
.redesign .rqcov-main{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:14px;align-items:baseline}
|
|
2245
|
+
.redesign .rqcov-text{font-size:13.5px;line-height:1.55;color:var(--ink)}
|
|
2246
|
+
.redesign .rqcov-state{font:600 10px/1.4 var(--mono);letter-spacing:.02em;color:var(--ink3);white-space:nowrap}
|
|
2247
|
+
.redesign .rqcov-state.verified{color:var(--green)}
|
|
2248
|
+
.redesign .rqcov-state.failed{color:var(--danger)}
|
|
2249
|
+
.redesign .rqcov-detail{margin-top:5px;font-size:11.5px;line-height:1.55;color:var(--ink3)}
|
|
2250
|
+
.redesign .rqcov-detail b{font-weight:600;color:var(--ink2)}
|
|
2235
2251
|
/* A long ask folds with the SAME .lacc pulldown Activity/Conversation use — chevron on
|
|
2236
2252
|
the peek line, body is the genuine rest (no duplicate, no trailing "…": Masa
|
|
2237
2253
|
2026-07-29 flagged both the vertical anchor line and the "…" as unnecessary — the
|
|
@@ -3278,6 +3294,7 @@
|
|
|
3278
3294
|
<div class="revactions">
|
|
3279
3295
|
<button type="button" class="reqbtn approve" id="reviewApprove">Approve goal</button>
|
|
3280
3296
|
<button type="button" class="reqbtn dismiss" id="reviewDismiss">Dismiss</button>
|
|
3297
|
+
<button type="button" class="reqbtn dismiss" id="reviewArchive" style="display:none">Archive</button>
|
|
3281
3298
|
<span style="flex:1"></span>
|
|
3282
3299
|
<button class="mbtn ghost" id="reviewClose">Close</button>
|
|
3283
3300
|
</div>
|
|
@@ -3686,6 +3703,18 @@ function retryCopy(projectId){
|
|
|
3686
3703
|
|
|
3687
3704
|
function esc(s){ const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; }
|
|
3688
3705
|
|
|
3706
|
+
// A PR closed on GitHub without merging is not a success — render it as
|
|
3707
|
+
// "Closed" (not the open/green treatment) instead of the usual "PR #N ↗".
|
|
3708
|
+
// `cls` is the base chip class (prbig/pr/ppr/sa-delivery); `.closed` is added
|
|
3709
|
+
// alongside it so each surface can style the muted state itself.
|
|
3710
|
+
function prChipHtml(pr, prClosed, cls){
|
|
3711
|
+
if (!pr) return '';
|
|
3712
|
+
const prNo = esc((pr.match(/pull\/(\d+)/) ?? [, ''])[1]);
|
|
3713
|
+
return prClosed
|
|
3714
|
+
? `<a class="${cls} closed" href="${esc(pr)}" target="_blank">PR #${prNo} — Closed</a>`
|
|
3715
|
+
: `<a class="${cls}" href="${esc(pr)}" target="_blank">PR #${prNo} ↗</a>`;
|
|
3716
|
+
}
|
|
3717
|
+
|
|
3689
3718
|
// Mirrors engine/lib.mjs dndDropsAfter()/reorderByDrop() — kept in sync
|
|
3690
3719
|
// manually since the browser script isn't a module and can't import
|
|
3691
3720
|
// server-side code.
|
|
@@ -3803,6 +3832,7 @@ function buildReviewDigest({ goals, tasks } = {}){
|
|
|
3803
3832
|
reviewNumber: goalReviewNumber({ goal: g, tasks }),
|
|
3804
3833
|
checkLine: plan ? `PLAN — approve to execute: ${checkLine}`.slice(0, 90) : checkLine,
|
|
3805
3834
|
pr: g.pr ?? null,
|
|
3835
|
+
prClosed: g.prClosed ?? false,
|
|
3806
3836
|
testResult: g.testResult ?? null,
|
|
3807
3837
|
verificationInfraError: g.verificationInfraError ?? null,
|
|
3808
3838
|
proof: proofTask?.proof ?? null,
|
|
@@ -4808,7 +4838,7 @@ function renderTasks(){
|
|
|
4808
4838
|
// the work went is logged inside the opened detail's "Also carries" section
|
|
4809
4839
|
// (see saItemHtml's carriesBody) instead — the list row stays just the ask.
|
|
4810
4840
|
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>
|
|
4811
|
-
<div class="cond">${
|
|
4841
|
+
<div class="cond">${prChipHtml(g.pr, g.prClosed, 'prbig')}</div>
|
|
4812
4842
|
<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>
|
|
4813
4843
|
<div class="revquick">
|
|
4814
4844
|
<button type="button" class="qbtn ok" data-qapprove="${g.id}" title="Approve">${ICONS.check}</button>
|
|
@@ -5453,14 +5483,18 @@ function renderReviewChecklist(){
|
|
|
5453
5483
|
? `REVIEW ${rn ?? '–'} / ${rlist.length || 1}`
|
|
5454
5484
|
: String(goal.status ?? '').toUpperCase();
|
|
5455
5485
|
$('reviewCount').innerHTML = countLabel
|
|
5456
|
-
+ (goal.pr ? ` ·
|
|
5486
|
+
+ (goal.pr ? ` · ${prChipHtml(goal.pr, goal.prClosed, 'prbig')}` : '');
|
|
5457
5487
|
// task: a goal opened from Attention (failed/interrupted) isn't reviewable —
|
|
5458
5488
|
// hide Approve/Dismiss goal entirely there (they read as nonsensical on a
|
|
5459
5489
|
// retry item), leaving just the report + Close. Per-requirement Approve/Dismiss
|
|
5460
5490
|
// are gated the same way in the card render below (canReview).
|
|
5461
5491
|
const canReview = goal.status === 'review';
|
|
5462
5492
|
$('reviewApprove').style.display = 'none'; // Feedback: drop the duplicate goal-level Approve — per-requirement Approve is enough
|
|
5463
|
-
|
|
5493
|
+
// The PR was closed on GitHub without merging — this isn't a rework case (Dismiss
|
|
5494
|
+
// sends it back for another attempt), it's already closed. Offer Archive instead
|
|
5495
|
+
// so the person can park it themselves rather than being sent through rework.
|
|
5496
|
+
$('reviewDismiss').style.display = canReview && !goal.prClosed ? '' : 'none';
|
|
5497
|
+
$('reviewArchive').style.display = canReview && goal.prClosed ? '' : 'none';
|
|
5464
5498
|
$('reviewApprove').disabled = !canReview;
|
|
5465
5499
|
// A folded goal opens here read-only (Masa 2026-07-23, [Open] from "Also carries"): nothing
|
|
5466
5500
|
// to approve, so don't show the approve-each-requirement intro — say honestly where its
|
|
@@ -5494,7 +5528,7 @@ function renderReviewChecklist(){
|
|
|
5494
5528
|
const lang = reviewLangFor(state.active);
|
|
5495
5529
|
// Action-framed so the first view IS "what you need to do" — read only this.
|
|
5496
5530
|
const L = { check: 'What you need to do', changed: 'What was done' };
|
|
5497
|
-
const prLink =
|
|
5531
|
+
const prLink = prChipHtml(goal.pr, goal.prClosed, 'prbig');
|
|
5498
5532
|
// goal 427 §4: change-risk — classifyChangeRisk (engine/lib.mjs) already
|
|
5499
5533
|
// decided this deterministically (no LLM) at goal-finish time; here we only
|
|
5500
5534
|
// render it. High = "never auto-approve" (the goal was already forced to
|
|
@@ -5698,9 +5732,13 @@ function celebrateApprove(){
|
|
|
5698
5732
|
}
|
|
5699
5733
|
|
|
5700
5734
|
// A merged PR is the terminal Review moment. Show the result in the dialog the
|
|
5701
|
-
// person is looking at, then
|
|
5702
|
-
//
|
|
5703
|
-
//
|
|
5735
|
+
// person is looking at, then drop just that completed review from the ledger —
|
|
5736
|
+
// the dialog itself stays open (task-939: closing it used to kick a person who
|
|
5737
|
+
// was mid-scroll through several review cards back out every time one PR
|
|
5738
|
+
// merged). saSync() rebuilds SA.items from live goal state (a 'done' goal drops
|
|
5739
|
+
// out of saLedgerRows on its own) and saRender()'s scroll-anchor logic keeps
|
|
5740
|
+
// whatever card the person was reading in view instead of jumping to the top.
|
|
5741
|
+
// Only close automatically once nothing is left to review.
|
|
5704
5742
|
function finishOpenReview(goal){
|
|
5705
5743
|
const ids = SA.items.flatMap((it) => it.ids || [it.goalId]);
|
|
5706
5744
|
if (!SA.open || !ids.includes(goal.id)) { celebrateApprove(); return; }
|
|
@@ -5722,7 +5760,12 @@ function finishOpenReview(goal){
|
|
|
5722
5760
|
celebrateApprove();
|
|
5723
5761
|
}
|
|
5724
5762
|
const delay = reduce ? 350 : 950;
|
|
5725
|
-
setTimeout(() => {
|
|
5763
|
+
setTimeout(() => {
|
|
5764
|
+
layer.remove();
|
|
5765
|
+
saSync(); // drops the now-done goal from SA.items and re-anchors the scroll in place
|
|
5766
|
+
if (SA.open && saPending().length === 0 && !SA.items.some((it) => it.st === 3)) saClose();
|
|
5767
|
+
render();
|
|
5768
|
+
}, delay);
|
|
5726
5769
|
}
|
|
5727
5770
|
// PR-less review approval: use the same high-signal green completion mark as
|
|
5728
5771
|
// the merged-PR path, but do not close or reflow the review surface. The canvas
|
|
@@ -6186,6 +6229,22 @@ async function sendReviewReject(){
|
|
|
6186
6229
|
} catch { showErr('Dismiss failed.'); }
|
|
6187
6230
|
}
|
|
6188
6231
|
$('reviewDismiss').onclick = () => sendReviewReject();
|
|
6232
|
+
// The PR closed on GitHub without merging — Archive parks the goal (same /archive
|
|
6233
|
+
// endpoint as the Ledger's Archive) instead of Dismiss's rework flow, since there
|
|
6234
|
+
// is nothing left to rework once the PR itself is closed.
|
|
6235
|
+
async function sendReviewArchive(){
|
|
6236
|
+
const goal = state.goals.find((g) => g.id === state.reviewOpenGoal);
|
|
6237
|
+
if (!goal) return;
|
|
6238
|
+
try {
|
|
6239
|
+
const r = await fetch(withKey(`/api/goals/${goal.id}/archive`), { method: 'POST' });
|
|
6240
|
+
if (!r.ok) throw new Error();
|
|
6241
|
+
const body = await r.json();
|
|
6242
|
+
upsert(state.goals, body.goal ?? body);
|
|
6243
|
+
closeReviewChecklist();
|
|
6244
|
+
render();
|
|
6245
|
+
} catch { showErr('Archive failed.'); }
|
|
6246
|
+
}
|
|
6247
|
+
$('reviewArchive').onclick = () => sendReviewArchive();
|
|
6189
6248
|
$('reviewFixSend').onclick = () => { const t = $('reviewFixInput').value.trim(); if (t || reviewAttach.length) sendReviewDismiss(t); };
|
|
6190
6249
|
$('reviewFixInput').addEventListener('keydown', (e) => {
|
|
6191
6250
|
if (e.key === 'Enter' && !e.isComposing) { const t = $('reviewFixInput').value.trim(); if (t || reviewAttach.length) sendReviewDismiss(t); }
|
|
@@ -9252,7 +9311,8 @@ function peraData(){
|
|
|
9252
9311
|
});
|
|
9253
9312
|
}
|
|
9254
9313
|
function peraPrLink(r){
|
|
9255
|
-
|
|
9314
|
+
if (!r.pr) return '';
|
|
9315
|
+
return prChipHtml(r.pr, r.prClosed, 'ppr').replace('<a ', '<a onclick="event.stopPropagation()" ');
|
|
9256
9316
|
}
|
|
9257
9317
|
function peraThumb(r, cls = ''){
|
|
9258
9318
|
return r.proofImg && r.proofTaskId != null
|
|
@@ -9426,6 +9486,33 @@ function latestGoalRunForOutcome(goalId, tasks, outcome){
|
|
|
9426
9486
|
note: prev.run?.note ? `${note} ${prev.run.note}` : note,
|
|
9427
9487
|
};
|
|
9428
9488
|
}
|
|
9489
|
+
function canonicalRequirementCoverage(goal, tasks){
|
|
9490
|
+
const requirements = Array.isArray(goal?.requirements) ? goal.requirements.filter((r) => r?.id && r?.text) : [];
|
|
9491
|
+
if (!requirements.length) {
|
|
9492
|
+
return (tasks || []).filter((t) => t.goalId === goal?.id && (t.detail || t.title) && (!t.reply || !t.replyFrom || t.replyFrom === 'you'))
|
|
9493
|
+
.sort((a, b) => (a.num ?? 0) - (b.num ?? 0))
|
|
9494
|
+
.map((t) => ({ id: `legacy-task-${t.id}`, text: String((t.reply ? t.detail : t.title) ?? t.title ?? '').replace(/\s+/g, ' ').trim(), coverage: 'unverified', legacy: true }));
|
|
9495
|
+
}
|
|
9496
|
+
const attemptOrder = new Map((goal.attempts || []).map((attempt, index) => [attempt.id, index]));
|
|
9497
|
+
const evidence = Array.isArray(goal.requirementEvidence) ? goal.requirementEvidence : [];
|
|
9498
|
+
return requirements.map((requirement) => {
|
|
9499
|
+
const rows = evidence.filter((row) => row?.requirementId === requirement.id)
|
|
9500
|
+
.sort((a, b) => (attemptOrder.get(a.attemptId) ?? -1) - (attemptOrder.get(b.attemptId) ?? -1));
|
|
9501
|
+
const latestAttemptId = rows.at(-1)?.attemptId;
|
|
9502
|
+
const latestRows = latestAttemptId ? rows.filter((row) => row.attemptId === latestAttemptId) : [];
|
|
9503
|
+
const statuses = latestRows.map((row) => row.coverage);
|
|
9504
|
+
const coverage = statuses.includes('failed') ? 'failed'
|
|
9505
|
+
: statuses.includes('verified') ? 'verified'
|
|
9506
|
+
: statuses.length && statuses.every((status) => status === 'not-applicable') ? 'not-applicable'
|
|
9507
|
+
: 'unverified';
|
|
9508
|
+
const claims = [...new Set(latestRows.map((row) => row.claim).filter(Boolean))];
|
|
9509
|
+
const workerEvidence = [...new Set(latestRows.map((row) => row.workerEvidence || row.evidence).filter(Boolean))];
|
|
9510
|
+
const checks = latestRows.flatMap((row) => Array.isArray(row.checks) ? row.checks : [])
|
|
9511
|
+
.filter((check) => Array.isArray(check.requirementIds) && check.requirementIds.includes(requirement.id));
|
|
9512
|
+
const checkFacts = [...new Set(checks.map((check) => `${check.kind}: ${check.status}`))];
|
|
9513
|
+
return { id: requirement.id, text: requirement.text, coverage, claims, workerEvidence, checkFacts, attemptId: latestAttemptId ?? null };
|
|
9514
|
+
});
|
|
9515
|
+
}
|
|
9429
9516
|
// One digest-row → one See-all card item. Extracted from saRows (2026-07-24) so a single
|
|
9430
9517
|
// "Needs you" goal can be opened as the SAME review card (saLedgerRows solo mode) — the card's
|
|
9431
9518
|
// whole shape (Request/Result/Proof/Activity, empty sections auto-hidden) is reused verbatim.
|
|
@@ -9475,15 +9562,13 @@ function saRowFromDigest(r, gs, ts){
|
|
|
9475
9562
|
// simply omit it, so the UI remains backward compatible during rollout.
|
|
9476
9563
|
attempt: g?.reviewAttempt ?? null,
|
|
9477
9564
|
unmappedChangedFiles: outcome?.unmappedChangedFiles ?? [],
|
|
9478
|
-
|
|
9479
|
-
requirementCoverage: ts.filter((t) => t.goalId === g?.id && (t.detail || t.title)).sort((a, b) => (a.num ?? 0) - (b.num ?? 0)).map((t) => ({ text: String((t.reply ? t.detail : t.title) ?? t.title ?? '').replace(/\s+/g, ' ').trim(), taskStatus: t.status, evidenceStatus: t.requirementEvidence?.requirements?.[0]?.status ?? null })),
|
|
9480
|
-
requirementEvidence: outcome?.requirementEvidence?.requirements ?? [],
|
|
9481
|
-
requirementCoverage: ts.filter((t) => t.goalId === g?.id && (t.detail || t.title) && (!t.reply || !t.replyFrom || t.replyFrom === 'you')).sort((a, b) => (a.num ?? 0) - (b.num ?? 0)).map((t) => ({ text: String((t.reply ? t.detail : t.title) ?? t.title ?? '').replace(/\s+/g, ' ').trim(), status: t.status })),
|
|
9565
|
+
requirementCoverage: canonicalRequirementCoverage(g, ts),
|
|
9482
9566
|
did, siblings, risk: g?.risk ?? null,
|
|
9483
9567
|
verificationInfraError: g?.verificationInfraError ?? r.verificationInfraError ?? null,
|
|
9484
9568
|
prNote: g?.prNote ?? null,
|
|
9485
9569
|
note: g?.retest?.passed ? g.retest.note : null,
|
|
9486
9570
|
pr: r.pr,
|
|
9571
|
+
prClosed: g?.prClosed ?? r.prClosed ?? false,
|
|
9487
9572
|
// P0b (Masa 2026-07-21): the AI's own TodoWrite breakdown for the "Steps" section
|
|
9488
9573
|
// below — the primary goal's own non-reply task, frozen at completion. Distinct
|
|
9489
9574
|
// from `did` above (task.result prose) and from Activity (raw tool-call log).
|
|
@@ -9590,7 +9675,7 @@ function saSoloDigestRow(g, ts){
|
|
|
9590
9675
|
const checkLine = (g.reviewSummary?.check || g.plan?.[0] || g.text || '').replace(/\s+/g, ' ').trim().slice(0, 66);
|
|
9591
9676
|
return {
|
|
9592
9677
|
goalIds: [g.id], reviewNumber: goalReviewNumber({ goal: g, tasks: ts }), checkLine,
|
|
9593
|
-
pr: g.pr ?? null, testResult: g.testResult ?? null,
|
|
9678
|
+
pr: g.pr ?? null, prClosed: g.prClosed ?? false, testResult: g.testResult ?? null,
|
|
9594
9679
|
verificationInfraError: g.verificationInfraError ?? null,
|
|
9595
9680
|
proof: proofTask?.proof ?? null, proofTaskId: proofTask?.id ?? null, plan: false, planText: null,
|
|
9596
9681
|
};
|
|
@@ -10033,31 +10118,18 @@ function saItemHtml(it, i){
|
|
|
10033
10118
|
const reqSec = reqStackRows
|
|
10034
10119
|
? `<section class="rsec rreq" data-reqmsgs="${gk}"><div class="rsec-h">Request</div><div class="rqlist">${reqStackRows}</div></section>`
|
|
10035
10120
|
: '';
|
|
10036
|
-
const reqVerify = it.requirementVerification;
|
|
10037
|
-
const reqVerifyParts = [
|
|
10038
|
-
reqVerify?.proof?.verified ? 'Independent proof recorded' : reqVerify?.proof ? 'Proof recorded' : '',
|
|
10039
|
-
reqVerify?.run?.url ? `Open it: ${reqVerify.run.url}` : '',
|
|
10040
|
-
Array.isArray(reqVerify?.changedFiles) && reqVerify.changedFiles.length ? `Files: ${reqVerify.changedFiles.join(' · ')}` : '',
|
|
10041
|
-
].filter(Boolean);
|
|
10042
|
-
const reqVerifySec = reqVerifyParts.length ? `<section class="rsec"><div class="rsec-h">Requirement verification</div><p class="rbody v">${esc(reqVerifyParts.join(' · '))}</p></section>` : '';
|
|
10043
10121
|
const coverage = (it.requirementCoverage || []).filter((r) => r.text);
|
|
10044
|
-
// For one requirement copied from Request, its status is useful but the full
|
|
10045
|
-
// sentence is not. Multiple or narrowed requirements retain their text.
|
|
10046
|
-
const coverageIsSingleRequest = coverage.length === 1
|
|
10047
|
-
&& String(coverage[0].text).replace(/\s+/g, ' ').trim() === requestText;
|
|
10048
10122
|
const coverageRows = coverage.map((r) => {
|
|
10049
|
-
const
|
|
10050
|
-
const
|
|
10051
|
-
|
|
10052
|
-
|
|
10053
|
-
|
|
10054
|
-
|
|
10055
|
-
|
|
10056
|
-
|
|
10057
|
-
const tone = entry.status === 'addressed' ? 'ok' : entry.status === 'partial' ? '' : 'dim';
|
|
10058
|
-
return `<p class="rbody v"><b class="${tone}">${esc(entry.status)}</b> · ${esc(entry.evidence || entry.id)}</p>`;
|
|
10123
|
+
const labels = { verified: 'Verified', failed: 'Failed', 'not-applicable': 'Not applicable', unverified: 'Not verified' };
|
|
10124
|
+
const details = [
|
|
10125
|
+
r.legacy ? 'Legacy item — no canonical evidence was recorded' : '',
|
|
10126
|
+
r.checkFacts?.length ? `Manager: ${r.checkFacts.join(' · ')}` : '',
|
|
10127
|
+
r.claims?.length ? `Worker claim: ${r.claims.join(', ')}` : '',
|
|
10128
|
+
r.workerEvidence?.length ? r.workerEvidence.join(' · ') : '',
|
|
10129
|
+
].filter(Boolean);
|
|
10130
|
+
return `<div class="rqcov-row" data-requirement-id="${esc(r.id)}"><div class="rqcov-main"><div class="rqcov-text">${esc(r.text)}</div><span class="rqcov-state ${esc(r.coverage)}">${esc(labels[r.coverage] || labels.unverified)}</span></div>${details.length ? `<div class="rqcov-detail">${details.map((detail, index) => index === 0 ? esc(detail) : `<br>${esc(detail)}`).join('')}</div>` : ''}</div>`;
|
|
10059
10131
|
}).join('');
|
|
10060
|
-
const
|
|
10132
|
+
const coverageSec = coverageRows ? `<section class="rsec"><div class="rsec-h">Requirement coverage</div><div class="rqcov">${coverageRows}</div></section>` : '';
|
|
10061
10133
|
// ── Open it (Masa 2026-07-22): a plain section in the card's own typography — the same
|
|
10062
10134
|
// heading + body shape as Request and Result, not a chip in another vocabulary. The
|
|
10063
10135
|
// address the worker declared IS the control (press it → the process starts on demand →
|
|
@@ -10296,9 +10368,8 @@ function saItemHtml(it, i){
|
|
|
10296
10368
|
: attn
|
|
10297
10369
|
? `<span class="rst ${attnInt ? 'int' : 'err'}"><span class="rdot"></span>${attnLabel}</span>`
|
|
10298
10370
|
: '<span class="rst"><span class="rdot"></span>In review</span>';
|
|
10299
|
-
const prNumber = (it.pr?.match(/\/pull\/(\d+)/) ?? [, ''])[1];
|
|
10300
10371
|
const delivery = it.pr
|
|
10301
|
-
?
|
|
10372
|
+
? prChipHtml(it.pr, it.prClosed, 'sa-delivery').replace('<a ', '<a rel="noopener" ')
|
|
10302
10373
|
: '<span class="sa-delivery">No pull request</span>';
|
|
10303
10374
|
return `<div class="sa-item lg redesign${it.goalId === SA.cur ? ' cur' : ''}${working ? ' working' : ''}" data-gid="${it.goalId}">
|
|
10304
10375
|
<div class="rhead"><span class="rtag">${it.no != null ? `#${it.no}` : ''}</span>${rst}</div>
|
|
@@ -10308,8 +10379,6 @@ function saItemHtml(it, i){
|
|
|
10308
10379
|
${S('Open it', openBody + infraBody)}
|
|
10309
10380
|
${S('What to check', checkBody)}
|
|
10310
10381
|
${coverageSec}
|
|
10311
|
-
${evidenceSec}
|
|
10312
|
-
${reqVerifySec}
|
|
10313
10382
|
${S('Run state', interruptedBody)}
|
|
10314
10383
|
${S('PR', prRepairBody)}
|
|
10315
10384
|
${S('Also carries', carriesBody)}
|
|
@@ -3,19 +3,94 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
-
<title>Review completion preview</title>
|
|
6
|
+
<title>Review completion preview — dialog stays open</title>
|
|
7
7
|
<style>
|
|
8
8
|
*{box-sizing:border-box}body{margin:0;background:#f4f5f7;color:#202124;font:14px Inter,ui-sans-serif,system-ui,sans-serif;min-height:100vh;display:grid;place-items:center}
|
|
9
|
-
.stage{width:min(900px,94vw);height:min(720px,88vh);position:relative}
|
|
9
|
+
.stage{width:min(900px,94vw);height:min(720px,88vh);position:relative}
|
|
10
|
+
.dialog{position:absolute;inset:0;background:#fff;border:1px solid #d9dce2;border-radius:16px;box-shadow:0 24px 70px rgba(22,25,31,.18);overflow:hidden;transition:opacity .2s,transform .2s;display:flex;flex-direction:column}
|
|
11
|
+
.dialog.closed{opacity:0;transform:scale(.985);pointer-events:none}
|
|
12
|
+
.head{height:58px;border-bottom:1px solid #e7e8eb;display:flex;align-items:center;padding:0 28px;font-weight:700;flex:none}
|
|
13
|
+
.head .c{margin-left:10px;font-weight:400;color:#737780;font-size:12px}
|
|
14
|
+
.body{flex:1;overflow:auto;padding:18px 28px}
|
|
15
|
+
.card{border:1px solid #e7e8eb;border-radius:12px;padding:20px 22px;margin-bottom:16px;position:relative;transition:opacity .25s,transform .25s}
|
|
16
|
+
.card.gone{opacity:0;transform:scale(.97);pointer-events:none;height:0;margin:0;padding:0;border:0;overflow:hidden}
|
|
17
|
+
.label{font:11px ui-monospace,SFMono-Regular,monospace;color:#737780;letter-spacing:.08em;text-transform:uppercase}
|
|
18
|
+
.title{font-size:18px;margin:9px 0 16px}
|
|
19
|
+
.line{height:11px;border-radius:3px;background:#eceef1;margin:10px 0}
|
|
20
|
+
.line.w1{width:82%}.line.w2{width:64%}.line.w3{width:73%}
|
|
21
|
+
.actions{margin-top:16px;display:flex;gap:10px}
|
|
22
|
+
.btn{height:34px;border-radius:9px;border:1px solid #d9dce2;background:#fff;padding:0 14px;cursor:pointer}
|
|
23
|
+
.btn.ok{background:#202124;color:#fff;border-color:#202124}
|
|
24
|
+
.replay{position:fixed;right:24px;bottom:24px;height:40px;padding:0 18px;border:0;border-radius:9px;background:#202124;color:#fff;font-weight:650;cursor:pointer}
|
|
25
|
+
.note{position:fixed;left:24px;bottom:24px;max-width:420px;font-size:12.5px;color:#4a4e57;background:#fff;border:1px solid #e7e8eb;border-radius:10px;padding:12px 14px}
|
|
26
|
+
.finish{position:absolute;inset:0;z-index:3;display:grid;place-items:center;overflow:hidden;border-radius:16px;background:rgba(245,247,250,.78);backdrop-filter:blur(3px);animation:fade .95s ease both}
|
|
27
|
+
.mark{width:88px;height:88px;border-radius:50%;display:grid;place-items:center;background:#18c77a;box-shadow:0 18px 52px rgba(24,199,122,.34);animation:pop .72s cubic-bezier(.2,1.35,.35,1) both}
|
|
28
|
+
.mark svg{width:48px;height:48px;fill:none;stroke:#fff;stroke-width:3.4;stroke-linecap:round;stroke-linejoin:round}
|
|
29
|
+
.mark path{stroke-dasharray:70;stroke-dashoffset:70;animation:draw .42s ease .18s forwards}
|
|
30
|
+
.piece{position:fixed;z-index:4;pointer-events:none;will-change:transform,opacity}
|
|
31
|
+
@keyframes pop{0%{transform:scale(.45);opacity:0}62%{transform:scale(1.1);opacity:1}100%{transform:scale(1)}}
|
|
32
|
+
@keyframes draw{to{stroke-dashoffset:0}}
|
|
33
|
+
@keyframes fade{0%,72%{opacity:1}100%{opacity:0}}
|
|
34
|
+
@media(prefers-reduced-motion:reduce){.finish,.mark,.mark path{animation:none}.finish{background:transparent}}
|
|
10
35
|
</style>
|
|
11
36
|
</head>
|
|
12
37
|
<body>
|
|
13
|
-
<main class="stage"
|
|
14
|
-
<
|
|
38
|
+
<main class="stage">
|
|
39
|
+
<section class="dialog" id="dialog">
|
|
40
|
+
<div class="head"><span>Review</span><span class="c" id="hdCount">3 ready</span></div>
|
|
41
|
+
<div class="body" id="body">
|
|
42
|
+
<div class="card" id="card-1" data-gid="1">
|
|
43
|
+
<div class="label">Request #1</div>
|
|
44
|
+
<h1 class="title">Fix the header spacing</h1>
|
|
45
|
+
<div class="line w1"></div><div class="line w2"></div><div class="line w3"></div>
|
|
46
|
+
<div class="actions"><button class="btn ok" data-merge="1">Merge PR</button><button class="btn">Dismiss</button></div>
|
|
47
|
+
</div>
|
|
48
|
+
<div class="card" id="card-2" data-gid="2">
|
|
49
|
+
<div class="label">Request #2</div>
|
|
50
|
+
<h1 class="title">Add empty-state illustration</h1>
|
|
51
|
+
<div class="line w1"></div><div class="line w2"></div><div class="line w3"></div>
|
|
52
|
+
<div class="actions"><button class="btn ok" data-merge="2">Merge PR</button><button class="btn">Dismiss</button></div>
|
|
53
|
+
</div>
|
|
54
|
+
<div class="card" id="card-3" data-gid="3">
|
|
55
|
+
<div class="label">Request #3</div>
|
|
56
|
+
<h1 class="title">Improve the approval completion moment</h1>
|
|
57
|
+
<div class="line w1"></div><div class="line w2"></div><div class="line w3"></div>
|
|
58
|
+
<div class="actions"><button class="btn ok" data-merge="3">Merge PR</button><button class="btn">Dismiss</button></div>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</section>
|
|
62
|
+
</main>
|
|
63
|
+
<button class="replay" id="replay">Reset all 3</button>
|
|
64
|
+
<div class="note">task-939: merging one PR (click "Merge PR" on any card) plays the completion
|
|
65
|
+
animation, then only that card is removed — the Review dialog stays open so you can keep
|
|
66
|
+
reading the other cards, instead of the whole dialog closing on you.</div>
|
|
15
67
|
<script>
|
|
16
68
|
const colors=['#F24E1E','#FF7262','#A259FF','#1ABCFE','#0ACF83','#FFD166'];
|
|
17
69
|
function burst(cx,cy){if(matchMedia('(prefers-reduced-motion: reduce)').matches)return;for(let i=0;i<42;i++){const p=document.createElement('i');p.className='piece';const s=6+Math.random()*6;p.style.cssText=`left:${cx}px;top:${cy}px;width:${s}px;height:${s}px;background:${colors[i%colors.length]};border-radius:${Math.random()<.5?'50%':'2px'};transition:transform .9s cubic-bezier(.15,.7,.3,1),opacity .9s ease .25s`;document.body.appendChild(p);const a=Math.random()*Math.PI*2,d=90+Math.random()*190,dx=Math.cos(a)*d,dy=Math.sin(a)*d*.6+90,rot=(Math.random()<.5?-1:1)*(360+Math.random()*360);requestAnimationFrame(()=>{p.style.transform=`translate(${dx}px,${dy}px) rotate(${rot}deg)`;p.style.opacity='0'});setTimeout(()=>p.remove(),1000)}}
|
|
18
|
-
function
|
|
19
|
-
|
|
70
|
+
function updateCount(){const left=document.querySelectorAll('.card:not(.gone)').length;document.getElementById('hdCount').textContent=left?`${left} ready`:'Nothing waiting for review';}
|
|
71
|
+
function mergeCard(gid){
|
|
72
|
+
const card=document.getElementById('card-'+gid);
|
|
73
|
+
if(!card||card.classList.contains('gone'))return;
|
|
74
|
+
const r=card.getBoundingClientRect();
|
|
75
|
+
const f=document.createElement('div');f.className='finish';f.style.cssText=`position:fixed;left:${r.left}px;top:${r.top}px;width:${r.width}px;height:${r.height}px;border-radius:12px;`;
|
|
76
|
+
f.innerHTML='<div class="mark"><svg viewBox="0 0 64 64"><path d="M17 33l10 10 21-23"/></svg></div>';
|
|
77
|
+
document.body.appendChild(f);
|
|
78
|
+
const mr=f.querySelector('.mark').getBoundingClientRect();
|
|
79
|
+
burst(mr.left+mr.width/2,mr.top+mr.height/2);
|
|
80
|
+
const delay=matchMedia('(prefers-reduced-motion: reduce)').matches?350:950;
|
|
81
|
+
setTimeout(()=>{
|
|
82
|
+
f.remove();
|
|
83
|
+
card.classList.add('gone'); // drop just this card — the dialog itself never closes here
|
|
84
|
+
updateCount();
|
|
85
|
+
},delay);
|
|
86
|
+
}
|
|
87
|
+
document.getElementById('body').addEventListener('click',(e)=>{
|
|
88
|
+
const gid=e.target?.dataset?.merge; if(gid) mergeCard(gid);
|
|
89
|
+
});
|
|
90
|
+
document.getElementById('replay').onclick=()=>{
|
|
91
|
+
document.querySelectorAll('.finish,.piece').forEach(x=>x.remove());
|
|
92
|
+
document.querySelectorAll('.card').forEach(c=>c.classList.remove('gone'));
|
|
93
|
+
updateCount();
|
|
94
|
+
};
|
|
20
95
|
</script>
|
|
21
96
|
</body></html>
|
package/engine/lib.mjs
CHANGED
|
@@ -2118,7 +2118,7 @@ export function buildReviewDigest({ goals, tasks } = {}) {
|
|
|
2118
2118
|
pr: g.pr ?? null,
|
|
2119
2119
|
testResult: g.testResult ?? null,
|
|
2120
2120
|
verificationInfraError: g.verificationInfraError ?? null,
|
|
2121
|
-
proof: goalTasks
|
|
2121
|
+
proof: latestProofTask(goalTasks)?.proof ?? null,
|
|
2122
2122
|
// Worker-declared partial/not-addressed requirements are not a failed
|
|
2123
2123
|
// verdict. Count them so a human does not miss them in the compact list.
|
|
2124
2124
|
incompleteRequirementCount: goalTasks.reduce((n, t) => n + (t.requirementEvidence?.requirements ?? []).filter((r) => ['partial', 'not-addressed'].includes(r.status)).length, 0),
|
|
@@ -2587,13 +2587,14 @@ export function isPrApproved({ reviewDecision } = {}) {
|
|
|
2587
2587
|
return reviewDecision === 'APPROVED';
|
|
2588
2588
|
}
|
|
2589
2589
|
|
|
2590
|
-
// A 'review' goal whose PR merged
|
|
2591
|
-
//
|
|
2592
|
-
//
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2590
|
+
// A 'review' goal whose PR merged is done. A PR closed WITHOUT merging is not
|
|
2591
|
+
// a success — it stays in 'review' (the caller marks goal.prClosed so the UI
|
|
2592
|
+
// can show "Closed", skip the done celebration, and let the person archive it
|
|
2593
|
+
// themselves) rather than being auto-celebrated as if it had shipped.
|
|
2594
|
+
// Every other goal status (including an approved but still-open PR) passes
|
|
2595
|
+
// through unchanged — this never regresses a goal out of 'review' on its own.
|
|
2596
|
+
export function reviewToDoneStatus({ goalStatus, merged }) {
|
|
2597
|
+
return goalStatus === 'review' && merged ? 'done' : goalStatus;
|
|
2597
2598
|
}
|
|
2598
2599
|
|
|
2599
2600
|
// Build the proof PR body (EN + JA, no emoji) and the PROOF.md committed to
|
|
@@ -3893,6 +3894,19 @@ export function buildGoalSummary({ goalText, tasks } = {}) {
|
|
|
3893
3894
|
return lines.join('\n');
|
|
3894
3895
|
}
|
|
3895
3896
|
|
|
3897
|
+
// The proof a reviewer is being asked to look at is the NEWEST one. A reply re-runs
|
|
3898
|
+
// the work and takes its own shot, so the goal accumulates several. This used to be
|
|
3899
|
+
// `tasks.find((t) => t.proof)`, which returns the OLDEST — the digest then carried
|
|
3900
|
+
// the first attempt's screenshot underneath the latest attempt's words, since the
|
|
3901
|
+
// outcome is picked by time (latestGoalOutcomeTask). Same fix as app/index.html's
|
|
3902
|
+
// latestProofTask, which this is mirrored by.
|
|
3903
|
+
export function latestProofTask(tasks = []) {
|
|
3904
|
+
return (tasks ?? [])
|
|
3905
|
+
.filter((t) => t?.proof)
|
|
3906
|
+
.sort((a, b) => Date.parse(b.finishedAt || b.startedAt || b.createdAt || 0)
|
|
3907
|
+
- Date.parse(a.finishedAt || a.startedAt || a.createdAt || 0))[0] ?? null;
|
|
3908
|
+
}
|
|
3909
|
+
|
|
3896
3910
|
// The Review card represents the goal's latest completed worker outcome. A reply is
|
|
3897
3911
|
// a real worker run and may supersede an earlier skipped/failed requirement.
|
|
3898
3912
|
export function latestGoalOutcomeTask(tasks = []) {
|
package/engine/server.mjs
CHANGED
|
@@ -1662,13 +1662,20 @@ async function syncGoalMerges() {
|
|
|
1662
1662
|
try { data = JSON.parse(raw); } catch { return; }
|
|
1663
1663
|
const merged = isPrMerged(data);
|
|
1664
1664
|
const closed = isPrClosed(data);
|
|
1665
|
-
const newStatus = reviewToDoneStatus({ goalStatus: g.status, merged
|
|
1666
|
-
|
|
1667
|
-
|
|
1665
|
+
const newStatus = reviewToDoneStatus({ goalStatus: g.status, merged });
|
|
1666
|
+
let changed = false;
|
|
1667
|
+
if (newStatus !== g.status) { g.status = newStatus; changed = true; }
|
|
1668
|
+
// A closed-without-merge PR is not a success — no done celebration. It stays
|
|
1669
|
+
// 'review' with prClosed set so the review dialog / PR links show "Closed"
|
|
1670
|
+
// (not the open/green treatment) and the person can Archive it themselves
|
|
1671
|
+
// instead of an automatic "down" done animation firing on their behalf.
|
|
1672
|
+
if (closed && !merged && !g.prClosed) { g.prClosed = true; changed = true; }
|
|
1673
|
+
if (changed) {
|
|
1674
|
+
saveGoal(g);
|
|
1668
1675
|
// §3/§7: a merged PR is truly terminal (no rework possible) — reclaim the
|
|
1669
1676
|
// per-goal worktree so .manager-wt/goal-<id> doesn't leak forever. (Manual
|
|
1670
1677
|
// Approve stays reworkable via revert, so we don't clean those up here.)
|
|
1671
|
-
if (
|
|
1678
|
+
if (merged || closed) {
|
|
1672
1679
|
const project = projects.find((p) => p.id === g.projectId);
|
|
1673
1680
|
if (project) removeGoalWorkDir(g, project);
|
|
1674
1681
|
}
|
package/package.json
CHANGED