@galda/cli 0.10.67 → 0.10.69
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 +69 -22
- package/engine/chrome-safety.mjs +25 -0
- package/engine/lib.mjs +19 -1
- package/engine/server.mjs +7 -4
- package/engine/shot.mjs +4 -0
- package/engine/verify.mjs +27 -6
- package/package.json +1 -1
package/app/index.html
CHANGED
|
@@ -5204,7 +5204,13 @@ function wireWfEditButtons(){
|
|
|
5204
5204
|
// /api/goals/:id/reply thread. Approvals live client-side only
|
|
5205
5205
|
// (state.reviewApprovals) since they are a review action, not new work.
|
|
5206
5206
|
function reviewRequirementsFor(goalId){
|
|
5207
|
-
|
|
5207
|
+
const reqs = state.tasks.filter((t) => t.goalId === goalId && !t.reply).sort((a, b) => (a.num ?? 0) - (b.num ?? 0));
|
|
5208
|
+
const outcome = latestGoalOutcomeTask(goalId, state.tasks);
|
|
5209
|
+
if (reqs.length === 1 && outcome?.reply) {
|
|
5210
|
+
const req = reqs[0];
|
|
5211
|
+
return [{ ...req, ...outcome, id: req.id, title: req.title, outcomeTaskId: outcome.id }];
|
|
5212
|
+
}
|
|
5213
|
+
return reqs;
|
|
5208
5214
|
}
|
|
5209
5215
|
function openReviewChecklist(goalId){
|
|
5210
5216
|
// H14 (Masa 2026-07-08): a single review IS the See-all Ledger focused on that
|
|
@@ -5356,7 +5362,7 @@ function renderReviewChecklist(){
|
|
|
5356
5362
|
}
|
|
5357
5363
|
// P1: one shot when a promised check actually captured one, otherwise nothing at
|
|
5358
5364
|
// all — "No capture yet" was us apologising for a photo we no longer go and take.
|
|
5359
|
-
const media = proofMediaHtml(t.id, t.proof, esc(t.title ?? ''));
|
|
5365
|
+
const media = proofMediaHtml(t.outcomeTaskId ?? t.id, t.proof, esc(t.title ?? ''));
|
|
5360
5366
|
// task 156/234 + 160: show the WORKER'S REPORT (what it actually did/found,
|
|
5361
5367
|
// incl. any test-only verification it describes) — that's what you read to
|
|
5362
5368
|
// decide. Falls back to the instruction if the worker left no result.
|
|
@@ -5933,18 +5939,33 @@ $('reviewOverlay').addEventListener('click', (e) => { if (e.target === $('review
|
|
|
5933
5939
|
// result linked to the goal (What was done), and the existing proof
|
|
5934
5940
|
// video/screenshot rendering (Proof). Approve/Dismiss and the revision send
|
|
5935
5941
|
// button are visual-only for now — not wired to any API.
|
|
5942
|
+
const REVIEW_DETAIL_STATUSES = new Set(['review', 'done', 'reverted', 'skipped', 'blocked', 'partial', 'failed', 'interrupted']);
|
|
5936
5943
|
function openGoalDetail(goalId){
|
|
5944
|
+
const g = state.goals.find((x) => x.id === goalId);
|
|
5945
|
+
// Completed and human-attention work always uses the current Review Ledger card.
|
|
5946
|
+
// Keeping this guard at the lowest-level opener prevents Done from falling back to
|
|
5947
|
+
// the legacy overlay when it is opened through a different list/log entry point.
|
|
5948
|
+
if (g && REVIEW_DETAIL_STATUSES.has(g.status) && typeof saOpen === 'function') {
|
|
5949
|
+
saOpen(goalId, true);
|
|
5950
|
+
return;
|
|
5951
|
+
}
|
|
5937
5952
|
state.goalDetailOpen = goalId;
|
|
5938
5953
|
renderGoalDetail();
|
|
5939
5954
|
$('goalDetailOverlay').classList.add('show');
|
|
5940
5955
|
}
|
|
5941
|
-
//
|
|
5942
|
-
// any other non-review goal keeps the goal-detail overlay.
|
|
5956
|
+
// All detail entry points share the routing in openGoalDetail().
|
|
5943
5957
|
function openGoalOrCard(goalId){
|
|
5944
|
-
const g = state.goals.find((x) => x.id === goalId);
|
|
5945
|
-
if (g && ['blocked', 'partial', 'failed', 'interrupted'].includes(g.status) && typeof saOpen === 'function') { saOpen(goalId, true); return; }
|
|
5946
5958
|
openGoalDetail(goalId);
|
|
5947
5959
|
}
|
|
5960
|
+
// List / board 共通の詳細導線。Review は review ledger、Needs you は attention card、
|
|
5961
|
+
// それ以外は通常の goal detail を開く。
|
|
5962
|
+
function fsOpenGoalDetail(goalId){
|
|
5963
|
+
const g = state.goals.find((x) => x.id === goalId);
|
|
5964
|
+
if (!g) return;
|
|
5965
|
+
goToGoalInChat(goalId);
|
|
5966
|
+
if (g.status === 'review') openReviewChecklist(goalId);
|
|
5967
|
+
else openGoalOrCard(goalId);
|
|
5968
|
+
}
|
|
5948
5969
|
function closeGoalDetail(){
|
|
5949
5970
|
$('goalDetailOverlay').classList.remove('show');
|
|
5950
5971
|
const goalId = state.goalDetailOpen;
|
|
@@ -6692,12 +6713,22 @@ function renderFsBoard(){
|
|
|
6692
6713
|
h += `<div class="lbc-meter"><span class="bar"><i style="width:${Math.round(done / total * 100)}%"></i></span><span class="frac">${done}/${total}</span></div>`;
|
|
6693
6714
|
}
|
|
6694
6715
|
}
|
|
6695
|
-
return `<div class="lbc${quiet ? ' is-quiet' : ''}">${h}</div>`;
|
|
6716
|
+
return `<div class="lbc${quiet ? ' is-quiet' : ''}" data-fsgoal="${g.id}">${h}</div>`;
|
|
6696
6717
|
};
|
|
6697
6718
|
const no = (g) => goalNo.get(g.id) ?? Infinity;
|
|
6698
6719
|
const bucket = { todo: [], doing: [], needs: [], review: [], done: [], later: [], rejected: [] };
|
|
6699
6720
|
for (const g of pgoals) bucket[fsBoardBucket(g, list)].push(g); // 'rejected' is collected but never rendered as a column below — closed goals disappear from the board
|
|
6700
6721
|
for (const k of Object.keys(bucket)) bucket[k].sort((a, b) => k === 'done' ? no(b) - no(a) : no(a) - no(b));
|
|
6722
|
+
// List と同じ digest を使い、同一 PR にまとまる Review は看板でも1件として扱う。
|
|
6723
|
+
const reviewDigest = buildReviewDigest({ goals: pgoals, tasks: list });
|
|
6724
|
+
const reviewCard = (r) => {
|
|
6725
|
+
const g0 = pgoals.find((g) => g.id === r.goalIds[0]);
|
|
6726
|
+
const n = r.reviewNumber;
|
|
6727
|
+
return `<div class="lbc" data-fsgoal="${r.goalIds[0]}"><div class="lbc-hd"><span class="lbc-dot rev"></span>`
|
|
6728
|
+
+ (n != null ? `<span class="lbc-n">#${n}</span>` : '')
|
|
6729
|
+
+ `</div><div class="lbc-t">${tt(r.plan ? r.checkLine : (g0?.text || g0?.plan?.[0] || r.checkLine), 90)}</div></div>`;
|
|
6730
|
+
};
|
|
6731
|
+
bucket.review = reviewDigest;
|
|
6701
6732
|
// To Do / Doing / Needs you / Review / Done / Later — the original design's columns
|
|
6702
6733
|
// (Masa 2026-07-16). "Needs you" = a clarifying question is waiting on the user —
|
|
6703
6734
|
// distinct from "blocked" (an error needing retry/archive, which stays in Doing, the
|
|
@@ -6706,11 +6737,15 @@ function renderFsBoard(){
|
|
|
6706
6737
|
col('To Do', 'var(--ink3)', bucket.todo.length, bucket.todo.map(goalCard).join(''), 'todo')
|
|
6707
6738
|
+ col('Doing', 'var(--green)', bucket.doing.length, bucket.doing.map(goalCard).join(''), 'doing')
|
|
6708
6739
|
+ (bucket.needs.length ? col('Needs you', '#ffca16', bucket.needs.length, bucket.needs.map(goalCard).join(''), 'needs') : '')
|
|
6709
|
-
+ col('Review', '#8390f2',
|
|
6740
|
+
+ col('Review', '#8390f2', reviewDigest.length, reviewDigest.map(reviewCard).join(''), 'review')
|
|
6710
6741
|
+ col('Done', 'var(--ink3)', bucket.done.length, bucket.done.slice(0, 40).map(goalCard).join(''), 'done')
|
|
6711
6742
|
+ col('Later', 'var(--ink3)', bucket.later.length, bucket.later.map(goalCard).join(''), 'later');
|
|
6712
6743
|
for (const b of el.querySelectorAll('[data-fsnew]')) b.onclick = () => $('input')?.focus(); // every + New / header + focuses the composer
|
|
6713
6744
|
for (const b of el.querySelectorAll('[data-fspause]')) b.onclick = togglePauseActive; // Doing列の一時停止/再生
|
|
6745
|
+
for (const c of el.querySelectorAll('[data-fsgoal]')) c.onclick = (e) => {
|
|
6746
|
+
if (e.target.closest('button, a')) return;
|
|
6747
|
+
fsOpenGoalDetail(Number(c.dataset.fsgoal));
|
|
6748
|
+
};
|
|
6714
6749
|
renderFsWait(bucket);
|
|
6715
6750
|
}
|
|
6716
6751
|
// Doing列の一時停止/再生 (Masa 2026-07-18・Spotify式トグル). 楽観的にローカルの
|
|
@@ -6934,7 +6969,7 @@ function renderFsTodo(){
|
|
|
6934
6969
|
// renders only when they exist. Same endpoints as the old layout's data-pstart/pdel:
|
|
6935
6970
|
// Start = POST /api/goals/:id/activate, Delete = DELETE /api/goals/:id (confirm).
|
|
6936
6971
|
// No mono # — pending goals are never planned, so they carry no task number yet.
|
|
6937
|
-
const laterRows = later.map((g) => `<div class="trow up dim"><span class="st"></span><span class="tt">${tt(g.text, 70)}</span>
|
|
6972
|
+
const laterRows = later.map((g) => `<div class="trow up dim" data-fsgoalrow="${g.id}"><span class="st"></span><span class="tt">${tt(g.text, 70)}</span>
|
|
6938
6973
|
<span class="acts"><button class="ab txt" data-fspstart="${g.id}" title="Queue it now">Start</button><button class="ab txt" data-fspdel="${g.id}" title="Delete">Delete</button></span></div>`).join('');
|
|
6939
6974
|
// reference numbers (HANDOFF-v45 §11): a #NN prefix on every To Do / Review item's name.
|
|
6940
6975
|
// Same goal = same number everywhere (goalReviewNumber = goal.id, unique & stable per
|
|
@@ -6949,11 +6984,11 @@ function renderFsTodo(){
|
|
|
6949
6984
|
running.filter((t) => !pausedGoalIds.has(t.goalId)).map((t) => {
|
|
6950
6985
|
const goal = pgoals.find((g) => g.id === t.goalId);
|
|
6951
6986
|
const title = t.reply ? (goal?.reviewSummary?.check || goal?.plan?.[0] || goal?.text || t.title) : t.title;
|
|
6952
|
-
return `<div class="drow"><div class="trow now"><span class="st run"></span>${noSpan(t.goalId)}<span class="tt">${tt(title)}</span><span class="pct num">${progressFor(t)}%</span>${fsActs(t.goalId)}</div>
|
|
6987
|
+
return `<div class="drow"><div class="trow now" data-fsgoalrow="${t.goalId}"><span class="st run"></span>${noSpan(t.goalId)}<span class="tt">${tt(title)}</span><span class="pct num">${progressFor(t)}%</span>${fsActs(t.goalId)}</div>
|
|
6953
6988
|
<div class="tbar"><i style="width:${progressFor(t)}%"></i></div></div>`;
|
|
6954
6989
|
}).join('')
|
|
6955
6990
|
// 一時停止したゴールは Doing に「Paused」1行(アンバー)で留める+Resume。走行/中断行は上と needsRows で抑止済み。
|
|
6956
|
-
+ pgoals.filter((g) => pausedGoalIds.has(g.id)).map((g) => `<div class="drow"><div class="trow now"><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('');
|
|
6991
|
+
+ 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('');
|
|
6957
6992
|
// NEEDS YOU: stopped/errored work that used to fold into Doing, now under its own caption so
|
|
6958
6993
|
// it no longer reads as "still in flight" (Masa 2026-07-19). Three kinds share it, each
|
|
6959
6994
|
// keeping the affordances it already had — nothing about the failed-task and blocked-goal
|
|
@@ -6978,13 +7013,13 @@ function renderFsTodo(){
|
|
|
6978
7013
|
const reason = String(t.result || '').trim();
|
|
6979
7014
|
const reasonLine = reason && reason.toLowerCase() !== word.toLowerCase()
|
|
6980
7015
|
? `<div class="msg"><span class="x ${isInt ? 'int' : ''}">${icon}</span>${tt(reason, 180)}</div>` : '';
|
|
6981
|
-
return `<div class="drow err${isInt ? ' int' : ''}"><div class="trow now"><span class="st run ${isInt ? 'int' : 'bad'}"></span>${noSpan(t.goalId)}<span class="tt">${tt(t.title)}</span><span class="stword ${isInt ? 'int' : ''}">${word}</span>${fsActs(t.goalId)}</div>
|
|
7016
|
+
return `<div class="drow err${isInt ? ' int' : ''}"><div class="trow now" data-fsgoalrow="${t.goalId}"><span class="st run ${isInt ? 'int' : 'bad'}"></span>${noSpan(t.goalId)}<span class="tt">${tt(t.title)}</span><span class="stword ${isInt ? 'int' : ''}">${word}</span>${fsActs(t.goalId)}</div>
|
|
6982
7017
|
<div class="errline">${reasonLine}
|
|
6983
7018
|
<div class="acts2"><button class="errbtn" data-fsretry="${t.id}">${label}</button><button class="errbtn" data-fslog="${t.id}">View log</button></div></div></div>`; }).join('')
|
|
6984
|
-
+ blocked.map((g) => g.blocked?.kind === 'paused' ? '' : `<div class="drow err"><div class="trow now"><span class="st run bad"></span>${noSpan(g.id)}<span class="tt">${tt(g.reviewSummary?.check || g.plan?.[0] || g.text, 60)}</span><span class="stword">Failed</span></div>
|
|
7019
|
+
+ blocked.map((g) => g.blocked?.kind === 'paused' ? '' : `<div class="drow err"><div class="trow now" data-fsgoalrow="${g.id}"><span class="st run bad"></span>${noSpan(g.id)}<span class="tt">${tt(g.reviewSummary?.check || g.plan?.[0] || g.text, 60)}</span><span class="stword">Failed</span></div>
|
|
6985
7020
|
<div class="errline"><div class="msg"><span class="x">✗</span>${tt(g.blocked?.reason ?? 'blocked', 180)}</div>
|
|
6986
7021
|
<div class="acts2"><button class="errbtn" data-fsreverify="${g.id}">Retry</button><button class="errbtn" data-fsglog="${g.id}">View log</button><button class="errbtn" data-fsarchive="${g.id}">Archive</button></div></div></div>`).join('')
|
|
6987
|
-
+ needsInput.map((g) => `<div class="drow err int"><div class="trow now"><span class="st run int"></span>${noSpan(g.id)}<span class="tt">${tt(g.text || g.plan?.[0], 60)}</span><span class="stword int">Needs input</span></div>
|
|
7022
|
+
+ needsInput.map((g) => `<div class="drow err int"><div class="trow now" data-fsgoalrow="${g.id}"><span class="st run int"></span>${noSpan(g.id)}<span class="tt">${tt(g.text || g.plan?.[0], 60)}</span><span class="stword int">Needs input</span></div>
|
|
6988
7023
|
<div class="errline"><div class="msg"><span class="x int">?</span>${tt(g.question?.text || 'A question is waiting on you', 180)}</div>
|
|
6989
7024
|
<div class="acts2"><button class="errbtn" data-fsglog="${g.id}">Answer</button></div></div></div>`).join('');
|
|
6990
7025
|
// Parked over-cap replies ride at the TOP of To Do (above the ordinary Next-up
|
|
@@ -6993,7 +7028,7 @@ function renderFsTodo(){
|
|
|
6993
7028
|
const canUseCodex = state.auth?.reason === 'rate-limited' && (state.agents || []).includes('codex');
|
|
6994
7029
|
const codexAction = (goalId, agent) => canUseCodex && agent === 'claude-code'
|
|
6995
7030
|
? `<button class="ab txt" data-fscodex="${goalId}" title="Start now with Codex">Run with Codex</button>` : '';
|
|
6996
|
-
const parkedRows = parkedReplyGoals.map((g) => `<div class="trow up" data-goalid="${g.id}"><span class="st"></span>${noSpan(g.id)}<span class="tt">${tt(g.reviewSummary?.check || g.plan?.[0] || g.text, 60)}</span><span class="stword" style="color:var(--ink3)">Queued</span>${codexAction(g.id, g.agent)}${fsActs(g.id)}</div>`).join('');
|
|
7031
|
+
const parkedRows = parkedReplyGoals.map((g) => `<div class="trow up" data-goalid="${g.id}" data-fsgoalrow="${g.id}"><span class="st"></span>${noSpan(g.id)}<span class="tt">${tt(g.reviewSummary?.check || g.plan?.[0] || g.text, 60)}</span><span class="stword" style="color:var(--ink3)">Queued</span>${codexAction(g.id, g.agent)}${fsActs(g.id)}</div>`).join('');
|
|
6997
7032
|
const NCAP = 6;
|
|
6998
7033
|
const upShown = fsUI.todoMore ? queued : queued.slice(0, NCAP);
|
|
6999
7034
|
const seenGoals = new Set();
|
|
@@ -7008,7 +7043,7 @@ function renderFsTodo(){
|
|
|
7008
7043
|
<div class="reps">${reps.map((r) => `<div class="rep">${tt(r.detail ?? r.title, 120)}</div>`).join('')}</div></div>`;
|
|
7009
7044
|
}
|
|
7010
7045
|
}
|
|
7011
|
-
return `<div class="trow up${i >= 2 ? ' dim' : ''}" data-goalid="${t.goalId ?? ''}" data-taskid="${t.id}"><span class="st"></span>${noSpan(t.goalId)}<span class="tt">${tt(t.title)}</span>${codexAction(t.goalId, t.agent)}${fsActs(t.goalId)}</div>${grp}`;
|
|
7046
|
+
return `<div class="trow up${i >= 2 ? ' dim' : ''}" data-goalid="${t.goalId ?? ''}" data-taskid="${t.id}"${t.goalId != null ? ` data-fsgoalrow="${t.goalId}"` : ''}><span class="st"></span>${noSpan(t.goalId)}<span class="tt">${tt(t.title)}</span>${codexAction(t.goalId, t.agent)}${fsActs(t.goalId)}</div>${grp}`;
|
|
7012
7047
|
}).join('');
|
|
7013
7048
|
// CDO review 2026-07-08 §7: this header count used to be queued.length alone (the
|
|
7014
7049
|
// "Next up" pool only) — so a project sitting in Doing/Later with an empty Next-up
|
|
@@ -7035,20 +7070,25 @@ function renderFsTodo(){
|
|
|
7035
7070
|
<div class="tl">
|
|
7036
7071
|
${(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}` : ''}
|
|
7037
7072
|
${needsRows ? `<span class="tlcap needs"${(doingRows || paused) ? '' : ' style="padding-top:0"'}>Needs you</span>${needsRows}` : ''}
|
|
7038
|
-
${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"><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('')}` : ''}
|
|
7073
|
+
${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('')}` : ''}
|
|
7039
7074
|
${(parkedRows || upRows) ? `<span class="tlcap"${(doingRows || paused) || needsRows || rejected.length ? '' : ' style="padding-top:0"'}>Next up</span>${parkedRows}${upRows}` : ''}
|
|
7040
7075
|
${queued.length > NCAP ? `<div class="morefold" id="fsTodoMore">${fsUI.todoMore ? 'less' : `+${queued.length - NCAP} more`}<span class="g">›</span></div>` : ''}
|
|
7041
7076
|
${laterRows ? `<span class="tlcap">Later</span>${laterRows}` : ''}
|
|
7042
7077
|
<button class="qadd" data-fsnew="1" title="New goal"><span class="paplus"></span>New</button>
|
|
7043
7078
|
</div>
|
|
7044
7079
|
${doneAll.length ? `<div class="doneline${fsUI.doneOpen ? ' open' : ''}" id="fsDoneLine">✓ Done<span class="num">${doneAll.length}</span><span class="fold">▾</span></div>
|
|
7045
|
-
<div id="fsDonelist"${fsUI.doneOpen ? '' : ' hidden'}>${doneAll.map((t) => `<div class="trow up dim done"><span class="st ok"></span>${noSpan(t.goalId)}<span class="tt">${tt(t.title)}</span></div>`).join('')}</div>` : ''}`;
|
|
7080
|
+
<div id="fsDonelist"${fsUI.doneOpen ? '' : ' hidden'}>${doneAll.map((t) => `<div class="trow up dim done"${t.goalId != null ? ` data-fsgoalrow="${t.goalId}"` : ''}><span class="st ok"></span>${noSpan(t.goalId)}<span class="tt">${tt(t.title)}</span></div>`).join('')}</div>` : ''}`;
|
|
7046
7081
|
el.classList.toggle('folded', !!fsUI.todoFold);
|
|
7047
7082
|
$('fsTodoHd').onclick = () => { fsUI.todoFold = !fsUI.todoFold; el.classList.toggle('folded', fsUI.todoFold); };
|
|
7048
7083
|
const qadd = el.querySelector('.qadd[data-fsnew]'); // + New → focus the composer (the real create path)
|
|
7049
7084
|
if (qadd) qadd.onclick = () => $('input')?.focus();
|
|
7050
7085
|
const dl = el.querySelector('#fsDoneLine');
|
|
7051
7086
|
if (dl) dl.onclick = () => { fsUI.doneOpen = !fsUI.doneOpen; dl.classList.toggle('open', fsUI.doneOpen); el.querySelector('#fsDonelist').hidden = !fsUI.doneOpen; };
|
|
7087
|
+
for (const r of el.querySelectorAll('[data-fsgoalrow]')) r.onclick = (e) => {
|
|
7088
|
+
if (e.target.closest('button, a, input, .acts, .acts2, .errbtn, .ab, .repfold')) return;
|
|
7089
|
+
const id = Number(r.dataset.fsgoalrow);
|
|
7090
|
+
if (Number.isFinite(id)) fsOpenGoalDetail(id);
|
|
7091
|
+
};
|
|
7052
7092
|
for (const b of el.querySelectorAll('[data-fsreopen]')) b.onclick = async (e) => { e.stopPropagation();
|
|
7053
7093
|
const r = await fetch(withKey(`/api/goals/${b.dataset.fsreopen}/reopen`), { method: 'POST' });
|
|
7054
7094
|
if (!r.ok) showErr('Reopen failed.'); else await refresh(); };
|
|
@@ -8788,20 +8828,27 @@ function saRows(){
|
|
|
8788
8828
|
const ts = (state.tasks || []).filter((t) => t.projectId === state.active);
|
|
8789
8829
|
return buildReviewDigest({ goals: gs, tasks: ts }).map((r) => saRowFromDigest(r, gs, ts));
|
|
8790
8830
|
}
|
|
8831
|
+
function latestGoalOutcomeTask(goalId, tasks){
|
|
8832
|
+
return (tasks || []).filter((t) => t.goalId === goalId && t.status === 'done' && String(t.result ?? '').trim())
|
|
8833
|
+
.sort((a, b) => Date.parse(b.finishedAt || b.startedAt || b.createdAt || 0)
|
|
8834
|
+
- Date.parse(a.finishedAt || a.startedAt || a.createdAt || 0))[0] || null;
|
|
8835
|
+
}
|
|
8791
8836
|
// One digest-row → one See-all card item. Extracted from saRows (2026-07-24) so a single
|
|
8792
8837
|
// "Needs you" goal can be opened as the SAME review card (saLedgerRows solo mode) — the card's
|
|
8793
8838
|
// whole shape (Request/Result/Proof/Activity, empty sections auto-hidden) is reused verbatim.
|
|
8794
8839
|
function saRowFromDigest(r, gs, ts){
|
|
8795
8840
|
const g = gs.find((x) => x.id === r.goalIds[0]);
|
|
8841
|
+
const outcome = latestGoalOutcomeTask(g?.id, ts);
|
|
8796
8842
|
const pt = ts.find((t) => r.goalIds.includes(t.goalId) && t.proof && (t.proof.png || t.proof.gif));
|
|
8797
8843
|
// "What we did" (H23 §2, replaces the old Plan/Review-thread folds): one bullet
|
|
8798
8844
|
// per finished requirement task's own report; falls back to the goal's plan
|
|
8799
8845
|
// steps when nothing has finished yet (e.g. a plan awaiting approval — the plan
|
|
8800
8846
|
// IS "what we're going to do" in that case, which is the honest content to show).
|
|
8801
|
-
const doneTasks = ts.filter((t) => r.goalIds.includes(t.goalId) && !t.reply && t.result)
|
|
8847
|
+
const doneTasks = ts.filter((t) => r.goalIds.includes(t.goalId) && !t.reply && t.result && t.status !== 'skipped')
|
|
8802
8848
|
.sort((a, b) => (a.num ?? 0) - (b.num ?? 0));
|
|
8803
8849
|
const did = doneTasks.length
|
|
8804
8850
|
? doneTasks.map((t) => String(t.result).replace(/\s+/g, ' ').trim().slice(0, 160))
|
|
8851
|
+
: outcome ? [String(outcome.result).replace(/\s+/g, ' ').trim().slice(0, 160)]
|
|
8805
8852
|
: (Array.isArray(g?.plan) ? g.plan.filter(Boolean).map((s) => String(s).replace(/\s+/g, ' ').trim()) : []);
|
|
8806
8853
|
// Sibling goals (H23 §2/§4 "did the AI drop part of what I asked"): buildReviewDigest
|
|
8807
8854
|
// already collapses goals that share one PR into a single row (goalIds.length > 1)
|
|
@@ -8856,7 +8903,7 @@ function saRowFromDigest(r, gs, ts){
|
|
|
8856
8903
|
// lines are NOT in state — 1000 of them per task would bloat every page load — so
|
|
8857
8904
|
// this is just the id to ask for when the reviewer opens Activity.
|
|
8858
8905
|
logTaskId: (() => {
|
|
8859
|
-
const t = ts.find((x) => x.goalId === g?.id &&
|
|
8906
|
+
const t = [...ts].reverse().find((x) => x.goalId === g?.id && x.activityLog);
|
|
8860
8907
|
return t?.id ?? null;
|
|
8861
8908
|
})(),
|
|
8862
8909
|
// P1 (Masa 2026-07-21): the AI's own final report, verbatim and untrimmed — the
|
|
@@ -8864,14 +8911,14 @@ function saRowFromDigest(r, gs, ts){
|
|
|
8864
8911
|
// every ask that has no running thing to open ("research this", "explain that"),
|
|
8865
8912
|
// and the explanation for every ask that does.
|
|
8866
8913
|
report: (() => {
|
|
8867
|
-
const t = ts.find((x) => x.goalId === g?.id && !x.reply && x.result);
|
|
8914
|
+
const t = outcome || ts.find((x) => x.goalId === g?.id && !x.reply && x.result);
|
|
8868
8915
|
return String(t?.result ?? '').replace(/^検証(PASS|FAIL)[^\n]*\n+/, '').trim();
|
|
8869
8916
|
})(),
|
|
8870
8917
|
// …and the worker's own "here's how you see it running" declaration (task.run,
|
|
8871
8918
|
// written by the worker into .manager-run.json). Absent for asks with nothing to
|
|
8872
8919
|
// run — then no button, which is the honest state, not a missing feature.
|
|
8873
8920
|
run: (() => {
|
|
8874
|
-
const t = ts.find((x) => x.goalId === g?.id &&
|
|
8921
|
+
const t = [...ts].reverse().find((x) => x.goalId === g?.id && x.run);
|
|
8875
8922
|
return t ? { taskId: t.id, ...t.run } : null;
|
|
8876
8923
|
})(),
|
|
8877
8924
|
// Explicit review-summary display fields (spec §11: the card's shape follows the
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
const MAC_CHROME = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
|
|
4
|
+
|
|
5
|
+
// Chrome updates its app bundle while an older copy can remain open. Launching
|
|
6
|
+
// the newly installed executable during that window aborts inside Chrome and
|
|
7
|
+
// macOS shows a scary "quit unexpectedly" dialog.
|
|
8
|
+
export function chromeUpdatePending(chromePath, {
|
|
9
|
+
platform = process.platform,
|
|
10
|
+
readCurrentVersion = () => execFileSync('/usr/libexec/PlistBuddy', [
|
|
11
|
+
'-c', 'Print:CFBundleShortVersionString',
|
|
12
|
+
'/Applications/Google Chrome.app/Contents/Info.plist',
|
|
13
|
+
], { encoding: 'utf8' }).trim(),
|
|
14
|
+
readProcesses = () => execFileSync('/bin/ps', ['-axo', 'command='], { encoding: 'utf8' }),
|
|
15
|
+
} = {}) {
|
|
16
|
+
if (platform !== 'darwin' || chromePath !== MAC_CHROME) return false;
|
|
17
|
+
try {
|
|
18
|
+
const current = readCurrentVersion();
|
|
19
|
+
const running = [...readProcesses().matchAll(/Google Chrome Framework\.framework\/Versions\/([^/\s]+)/g)]
|
|
20
|
+
.map((match) => match[1]);
|
|
21
|
+
return running.some((version) => version !== current);
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
package/engine/lib.mjs
CHANGED
|
@@ -2585,6 +2585,7 @@ export function workerPrompt(task, goal, lastFailure, agentName = 'claude-code')
|
|
|
2585
2585
|
// instead of expanding scope"): the request, not our fear of scope, sets
|
|
2586
2586
|
// the size of the work.
|
|
2587
2587
|
'- Deliver what the request actually asks for. If it asks for multiple options, N variations, a visual, an artifact, or a full exploration, produce ALL of them — do not reduce it to one, and do not cut it short to finish faster.',
|
|
2588
|
+
'- デザイン案・UI案・レイアウト案を求められた場合は、文章だけで完了にしない。画像を使わない案でも、比較して確認できるHTML等のartifactをプロジェクト内に作り、実際に開ける .manager-run.json を残す。',
|
|
2588
2589
|
'- Keep the work bounded to the request: inspect only files needed; avoid broad repository scans unless they are necessary.',
|
|
2589
2590
|
"- You may use the user's installed skills (Skill tool, e.g. /galda-doc) when one clearly fits the request.",
|
|
2590
2591
|
// Token-frugality + the 10-min run cap: the worker used to run the WHOLE suite
|
|
@@ -2972,7 +2973,15 @@ export function visibleTodos(list, showAll, limit = 10) {
|
|
|
2972
2973
|
// Japanese lines covering ①何をやったか ②変更点 ③確認方法, even when tasks
|
|
2973
2974
|
// are mixed done/failed/interrupted or there is only a single task.
|
|
2974
2975
|
export function buildGoalSummary({ goalText, tasks } = {}) {
|
|
2975
|
-
const
|
|
2976
|
+
const all = tasks ?? [];
|
|
2977
|
+
const requirements = all.filter((t) => !t.reply);
|
|
2978
|
+
const latestOutcome = latestGoalOutcomeTask(all);
|
|
2979
|
+
// A follow-up can be the run that finally delivered the work after the original
|
|
2980
|
+
// requirement was skipped/interrupted. Keep the requirement count stable, but
|
|
2981
|
+
// summarize the effective outcome instead of preserving a stale "Skipped.".
|
|
2982
|
+
const work = requirements.length === 1 && latestOutcome?.reply
|
|
2983
|
+
? [{ ...requirements[0], ...latestOutcome, title: requirements[0].title }]
|
|
2984
|
+
: requirements;
|
|
2976
2985
|
const done = work.filter((t) => ['done', 'skipped'].includes(t.status));
|
|
2977
2986
|
const failed = work.filter((t) => ['failed', 'interrupted'].includes(t.status));
|
|
2978
2987
|
const files = [...new Set(work.flatMap((t) => t.changedFiles ?? []))];
|
|
@@ -3009,6 +3018,15 @@ export function buildGoalSummary({ goalText, tasks } = {}) {
|
|
|
3009
3018
|
return lines.join('\n');
|
|
3010
3019
|
}
|
|
3011
3020
|
|
|
3021
|
+
// The Review card represents the goal's latest completed worker outcome. A reply is
|
|
3022
|
+
// a real worker run and may supersede an earlier skipped/failed requirement.
|
|
3023
|
+
export function latestGoalOutcomeTask(tasks = []) {
|
|
3024
|
+
return tasks
|
|
3025
|
+
.filter((t) => t && t.status === 'done' && String(t.result ?? '').trim())
|
|
3026
|
+
.sort((a, b) => Date.parse(b.finishedAt || b.startedAt || b.createdAt || 0)
|
|
3027
|
+
- Date.parse(a.finishedAt || a.startedAt || a.createdAt || 0))[0] ?? null;
|
|
3028
|
+
}
|
|
3029
|
+
|
|
3012
3030
|
// Plain-text snapshot of Review/Attention/Done fed to the /api/ask LLM pass —
|
|
3013
3031
|
// same three buckets app/index.html's renderTasks() renders (goal.status
|
|
3014
3032
|
// 'review'/'blocked' + task.status failed/interrupted + task.status
|
package/engine/server.mjs
CHANGED
|
@@ -2363,12 +2363,15 @@ async function runTask(task) {
|
|
|
2363
2363
|
if (task.reply) {
|
|
2364
2364
|
const plan = goal?.executionPlan;
|
|
2365
2365
|
const resumeSession = shouldFreshSessionForPlan(plan) ? undefined : (goal?.sessionId ?? undefined);
|
|
2366
|
+
// A follow-up is a full worker run, so it needs the same deliverable contract as
|
|
2367
|
+
// the first attempt (artifacts, run declaration, tests, language). The old short
|
|
2368
|
+
// prompt omitted those rules, which let a reply finish with prose only and left
|
|
2369
|
+
// Review showing the original task's stale "Skipped." result.
|
|
2366
2370
|
const prompt = [
|
|
2367
2371
|
managerHandoffPrompt(goal, plan),
|
|
2368
|
-
task.
|
|
2369
|
-
'',
|
|
2370
|
-
|
|
2371
|
-
].filter(Boolean).join('\n');
|
|
2372
|
+
workerPrompt(task, goal, null, workerAgent(task.agent ?? goal?.agent)),
|
|
2373
|
+
'(User follow-up on the goal you worked on. Apply it to the same goal and deliver the requested result.)',
|
|
2374
|
+
].filter(Boolean).join('\n\n');
|
|
2372
2375
|
const callT0 = Date.now();
|
|
2373
2376
|
sendProcessAct(task, `Starting ${workerAgent(task.agent ?? goal?.agent) === 'codex' ? 'Codex' : 'Claude Code'} worker with the existing goal context.`);
|
|
2374
2377
|
const r = await runWorkerAgent({
|
package/engine/shot.mjs
CHANGED
|
@@ -21,6 +21,7 @@ import { existsSync, mkdirSync, statSync } from 'node:fs';
|
|
|
21
21
|
import { dirname, resolve } from 'node:path';
|
|
22
22
|
import { createRequire } from 'node:module';
|
|
23
23
|
import { parseShotArgs, shotTargetUrl, pickChromePath } from './lib.mjs';
|
|
24
|
+
import { chromeUpdatePending } from './chrome-safety.mjs';
|
|
24
25
|
|
|
25
26
|
const require = createRequire(import.meta.url);
|
|
26
27
|
|
|
@@ -42,6 +43,9 @@ const url = shotTargetUrl(opt.target, (p) => resolve(process.cwd(), p));
|
|
|
42
43
|
const out = resolve(process.cwd(), opt.out);
|
|
43
44
|
const chrome = pickChromePath(process.env, existsSync);
|
|
44
45
|
if (!chrome) die('Chrome not found — install Google Chrome or set CHROME_PATH to the binary');
|
|
46
|
+
if (process.env.GALDA_TEST_ALLOW_PENDING_CHROME !== '1' && chromeUpdatePending(chrome)) {
|
|
47
|
+
die('Chrome just updated while it was open. Restart Chrome, then retry the screenshot.');
|
|
48
|
+
}
|
|
45
49
|
|
|
46
50
|
let puppeteer;
|
|
47
51
|
try {
|
package/engine/verify.mjs
CHANGED
|
@@ -6,10 +6,14 @@
|
|
|
6
6
|
|
|
7
7
|
import { spawnSync } from 'node:child_process';
|
|
8
8
|
import { createRequire } from 'node:module';
|
|
9
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
10
|
+
import { tmpdir } from 'node:os';
|
|
11
|
+
import { join } from 'node:path';
|
|
9
12
|
|
|
10
13
|
const require = createRequire(import.meta.url);
|
|
11
14
|
const puppeteer = require('puppeteer-core');
|
|
12
15
|
import { existsSync } from 'node:fs';
|
|
16
|
+
import { chromeUpdatePending } from './chrome-safety.mjs';
|
|
13
17
|
|
|
14
18
|
const CHROME_CANDIDATES = [
|
|
15
19
|
process.env.CHROME_PATH,
|
|
@@ -109,12 +113,28 @@ export async function exerciseUi(page, ui) {
|
|
|
109
113
|
// still screenshot is produced) — used for a cheap baseline ("before") shot
|
|
110
114
|
// where a comparison image is enough and a second full video isn't worth
|
|
111
115
|
// the extra time/flakiness.
|
|
112
|
-
export async function runVerification({
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
116
|
+
export async function runVerification({
|
|
117
|
+
entryUrl, verify, outBase, record = true, viewport = null,
|
|
118
|
+
browserUpdatePending = () => chromeUpdatePending(CHROME),
|
|
119
|
+
}) {
|
|
120
|
+
if (browserUpdatePending()) {
|
|
121
|
+
return {
|
|
122
|
+
pass: false,
|
|
123
|
+
inconclusive: true,
|
|
124
|
+
detail: 'Chrome was updated while it was open. Automatic browser verification was safely deferred; restart Chrome to enable it.',
|
|
125
|
+
shotPath: null,
|
|
126
|
+
videoPath: null,
|
|
127
|
+
gifPath: null,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const userDataDir = mkdtempSync(join(tmpdir(), 'galda-verify-chrome-'));
|
|
131
|
+
let browser;
|
|
117
132
|
try {
|
|
133
|
+
browser = await puppeteer.launch({
|
|
134
|
+
executablePath: CHROME, headless: 'new',
|
|
135
|
+
userDataDir,
|
|
136
|
+
args: ['--no-first-run', '--hide-scrollbars', '--disable-crash-reporter'],
|
|
137
|
+
});
|
|
118
138
|
const page = await browser.newPage();
|
|
119
139
|
await page.setViewport(viewport ?? { width: 900, height: 640, deviceScaleFactor: 2 });
|
|
120
140
|
const consoleErrors = [];
|
|
@@ -160,6 +180,7 @@ export async function runVerification({ entryUrl, verify, outBase, record = true
|
|
|
160
180
|
if (consoleErrors.length) result.detail += ` | page errors: ${consoleErrors.join(' / ').slice(0, 300)}`;
|
|
161
181
|
return { ...result, shotPath, videoPath: finalVideo, gifPath };
|
|
162
182
|
} finally {
|
|
163
|
-
await browser
|
|
183
|
+
await browser?.close().catch(() => {});
|
|
184
|
+
rmSync(userDataDir, { recursive: true, force: true });
|
|
164
185
|
}
|
|
165
186
|
}
|
package/package.json
CHANGED