@galda/cli 0.10.68 → 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 +29 -10
- 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,16 +5939,22 @@ $('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
|
}
|
|
5948
5960
|
// List / board 共通の詳細導線。Review は review ledger、Needs you は attention card、
|
|
@@ -8816,20 +8828,27 @@ function saRows(){
|
|
|
8816
8828
|
const ts = (state.tasks || []).filter((t) => t.projectId === state.active);
|
|
8817
8829
|
return buildReviewDigest({ goals: gs, tasks: ts }).map((r) => saRowFromDigest(r, gs, ts));
|
|
8818
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
|
+
}
|
|
8819
8836
|
// One digest-row → one See-all card item. Extracted from saRows (2026-07-24) so a single
|
|
8820
8837
|
// "Needs you" goal can be opened as the SAME review card (saLedgerRows solo mode) — the card's
|
|
8821
8838
|
// whole shape (Request/Result/Proof/Activity, empty sections auto-hidden) is reused verbatim.
|
|
8822
8839
|
function saRowFromDigest(r, gs, ts){
|
|
8823
8840
|
const g = gs.find((x) => x.id === r.goalIds[0]);
|
|
8841
|
+
const outcome = latestGoalOutcomeTask(g?.id, ts);
|
|
8824
8842
|
const pt = ts.find((t) => r.goalIds.includes(t.goalId) && t.proof && (t.proof.png || t.proof.gif));
|
|
8825
8843
|
// "What we did" (H23 §2, replaces the old Plan/Review-thread folds): one bullet
|
|
8826
8844
|
// per finished requirement task's own report; falls back to the goal's plan
|
|
8827
8845
|
// steps when nothing has finished yet (e.g. a plan awaiting approval — the plan
|
|
8828
8846
|
// IS "what we're going to do" in that case, which is the honest content to show).
|
|
8829
|
-
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')
|
|
8830
8848
|
.sort((a, b) => (a.num ?? 0) - (b.num ?? 0));
|
|
8831
8849
|
const did = doneTasks.length
|
|
8832
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)]
|
|
8833
8852
|
: (Array.isArray(g?.plan) ? g.plan.filter(Boolean).map((s) => String(s).replace(/\s+/g, ' ').trim()) : []);
|
|
8834
8853
|
// Sibling goals (H23 §2/§4 "did the AI drop part of what I asked"): buildReviewDigest
|
|
8835
8854
|
// already collapses goals that share one PR into a single row (goalIds.length > 1)
|
|
@@ -8884,7 +8903,7 @@ function saRowFromDigest(r, gs, ts){
|
|
|
8884
8903
|
// lines are NOT in state — 1000 of them per task would bloat every page load — so
|
|
8885
8904
|
// this is just the id to ask for when the reviewer opens Activity.
|
|
8886
8905
|
logTaskId: (() => {
|
|
8887
|
-
const t = ts.find((x) => x.goalId === g?.id &&
|
|
8906
|
+
const t = [...ts].reverse().find((x) => x.goalId === g?.id && x.activityLog);
|
|
8888
8907
|
return t?.id ?? null;
|
|
8889
8908
|
})(),
|
|
8890
8909
|
// P1 (Masa 2026-07-21): the AI's own final report, verbatim and untrimmed — the
|
|
@@ -8892,14 +8911,14 @@ function saRowFromDigest(r, gs, ts){
|
|
|
8892
8911
|
// every ask that has no running thing to open ("research this", "explain that"),
|
|
8893
8912
|
// and the explanation for every ask that does.
|
|
8894
8913
|
report: (() => {
|
|
8895
|
-
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);
|
|
8896
8915
|
return String(t?.result ?? '').replace(/^検証(PASS|FAIL)[^\n]*\n+/, '').trim();
|
|
8897
8916
|
})(),
|
|
8898
8917
|
// …and the worker's own "here's how you see it running" declaration (task.run,
|
|
8899
8918
|
// written by the worker into .manager-run.json). Absent for asks with nothing to
|
|
8900
8919
|
// run — then no button, which is the honest state, not a missing feature.
|
|
8901
8920
|
run: (() => {
|
|
8902
|
-
const t = ts.find((x) => x.goalId === g?.id &&
|
|
8921
|
+
const t = [...ts].reverse().find((x) => x.goalId === g?.id && x.run);
|
|
8903
8922
|
return t ? { taskId: t.id, ...t.run } : null;
|
|
8904
8923
|
})(),
|
|
8905
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