@galda/cli 0.10.39 → 0.10.40
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 +267 -54
- package/engine/lib.mjs +107 -1
- package/engine/server.mjs +27 -32
- package/package.json +1 -1
package/app/index.html
CHANGED
|
@@ -2028,6 +2028,20 @@
|
|
|
2028
2028
|
/* "Open it" is waiting on a real process to boot (up to ~15s), so the chip says so
|
|
2029
2029
|
rather than looking dead. Dim + no pointer, no spinner — §5.5 薄く・主張しない. */
|
|
2030
2030
|
.redesign .mx.mwait{opacity:.55;pointer-events:none}
|
|
2031
|
+
/* the AI's own screenshots: full width, own aspect, hairline edge (§5.5 縁は1px以下).
|
|
2032
|
+
No grid, no crop — one shot is often a comparison sheet and dies when shrunk. */
|
|
2033
|
+
.redesign .rshot{margin:0 0 16px}
|
|
2034
|
+
.redesign .rshot:last-child{margin-bottom:0}
|
|
2035
|
+
.redesign .rshot img{display:block;width:100%;height:auto;border-radius:10px;border:1px solid var(--hair)}
|
|
2036
|
+
.redesign .rshot figcaption{margin-top:6px;font-family:var(--mono);font-size:11px;color:var(--ink3)}
|
|
2037
|
+
/* before/after pair + the red box over what actually changed (pixel-diffed here from
|
|
2038
|
+
the worker's own two shots — no box appears unless the pixels really differ) */
|
|
2039
|
+
.redesign .beforeafter .proofimg{width:100%;height:auto;max-height:none;object-fit:initial;display:block;border-radius:8px;border:1px solid var(--hair)}
|
|
2040
|
+
.redesign .ba-imgwrap{position:relative}
|
|
2041
|
+
.redesign .ba-imgwrap .proofimg{margin-top:0}
|
|
2042
|
+
.redesign .ba-box{position:absolute;border:1.5px solid var(--danger);border-radius:2px;pointer-events:none}
|
|
2043
|
+
.redesign .ba-zoomhd{margin:14px 0 8px;font-family:var(--mono);font-size:11px;letter-spacing:.04em;text-transform:uppercase;color:var(--ink3)}
|
|
2044
|
+
.redesign .ba-zoomimg{display:block;width:100%;height:auto;border-radius:8px;border:1px solid var(--hair)}
|
|
2031
2045
|
.redesign .rfiles{margin:12px 0 0;font-family:var(--mono);font-size:12px;color:var(--ink2);line-height:1.75}
|
|
2032
2046
|
.redesign .rfiles .a{color:#3fd07a;font-style:normal}.redesign .rfiles .d{color:#ff7b72;font-style:normal}
|
|
2033
2047
|
/* Request accordion (spec §3) — chevron › rotate 90, body indented, no left rule */
|
|
@@ -2100,22 +2114,25 @@
|
|
|
2100
2114
|
in at the bottom of the opened body. */
|
|
2101
2115
|
.redesign .ract{margin-top:24px !important}
|
|
2102
2116
|
.redesign .ract>.rsec-h{margin-bottom:10px !important}
|
|
2103
|
-
.redesign .lacc>summary{list-style:none;cursor:pointer;display:flex;gap:
|
|
2117
|
+
.redesign .lacc>summary{list-style:none;cursor:pointer;display:flex;gap:14px;align-items:baseline}
|
|
2104
2118
|
.redesign .lacc>summary::-webkit-details-marker{display:none}
|
|
2105
|
-
.redesign .lacc .ltog{flex:0 0 auto;color:var(--
|
|
2119
|
+
.redesign .lacc .ltog{flex:0 0 auto;color:var(--ink2);font-style:normal;font-size:1.15em;line-height:1.25;transition:transform .14s}
|
|
2106
2120
|
.redesign .lacc[open]>summary .ltog{transform:rotate(90deg)}
|
|
2107
2121
|
.redesign .lacc .lfirst{flex:1;min-width:0}
|
|
2108
2122
|
.redesign .lacc .lbody{margin-top:12px;padding-left:18px}
|
|
2109
2123
|
/* transcript rows — Linear-clean, borderless (You / tool mono / res green / Claude) */
|
|
2110
2124
|
.redesign .rtx{border:0;background:none}
|
|
2111
|
-
.redesign .txr{display:flex;gap:12px;padding:5px 0;font-size:13px;line-height:1.55}
|
|
2112
|
-
.redesign .txr .w{flex:0 0 46px;font-size:11px;color:var(--ink3)
|
|
2125
|
+
.redesign .txr{display:flex;gap:12px;padding:5px 0;font-size:13px;line-height:1.55;align-items:baseline}
|
|
2126
|
+
.redesign .txr .w{flex:0 0 46px;font-size:11px;color:var(--ink3)}
|
|
2113
2127
|
.redesign .txr.you .w,.redesign .txr.cl .w{color:var(--ink);font-weight:600}
|
|
2114
2128
|
.redesign .txr .m{flex:1;min-width:0;color:var(--ink2)}
|
|
2115
2129
|
.redesign .txr.cl .m{color:var(--ink)}
|
|
2116
2130
|
.redesign .txr.tool .m{color:var(--ink2);font-family:var(--mono);font-size:12px}
|
|
2117
2131
|
.redesign .txr.tool .m b{color:var(--ink);font-weight:600}
|
|
2118
2132
|
.redesign .txr.res .m{font-family:var(--mono);font-size:12px;color:var(--green)}
|
|
2133
|
+
/* Manager's own progress notes inside the implementation log: commentary, not results.
|
|
2134
|
+
Green would read as "passed" (§5.5 UIクロームは薄く・優しく・主張しない). */
|
|
2135
|
+
.redesign .txr.note .m{font-family:var(--mono);font-size:12px;color:var(--ink3)}
|
|
2119
2136
|
/* Discussion (review back-and-forth) folded at the bottom of Activity's body */
|
|
2120
2137
|
.redesign .lbody .rthread{display:flex;flex-direction:column;gap:16px;margin:0}
|
|
2121
2138
|
.redesign .lbody .rmsg .who{font-size:12px;font-weight:600;color:var(--ink);margin-bottom:4px}
|
|
@@ -3758,11 +3775,105 @@ function proofMediaHtml(taskId, proof, altLabel){
|
|
|
3758
3775
|
// us, not by the work: the "before" shot came from the same photo-staging pass that P1
|
|
3759
3776
|
// removed on the engine side (#228), and in practice it never existed at all, so every
|
|
3760
3777
|
// card fell through to the single shot anyway. Callers now ask for that shot directly.
|
|
3761
|
-
// (
|
|
3762
|
-
//
|
|
3763
|
-
//
|
|
3764
|
-
//
|
|
3765
|
-
//
|
|
3778
|
+
// Red box (restored 2026-07-22 on Masa's ask). This machinery was deleted in #230 with
|
|
3779
|
+
// the staged before/after pair, because the 'before' was a photo WE took of a screen
|
|
3780
|
+
// that had nothing to do with the change. It comes back on a different footing: the two
|
|
3781
|
+
// images are both the worker's own shots, taken by the agent that did the work, and the
|
|
3782
|
+
// box is computed here from those two PNGs by pixel comparison — nothing is invented, a
|
|
3783
|
+
// box only appears when the pixels really differ in one region.
|
|
3784
|
+
// minCoverage lowered 2026-07-22: the old 0.05% floor was tuned on a 640x200 proof pair.
|
|
3785
|
+
// A real worker shot is a whole 900-1440px page, where a genuine change (a dot going
|
|
3786
|
+
// green, a label swapping) is well under that floor and was being thrown away as noise.
|
|
3787
|
+
// 30 pixels differing by more than `threshold` is a change someone made on purpose;
|
|
3788
|
+
// compression noise does not clear a 24-point RGB delta.
|
|
3789
|
+
function computeDiffBox(beforeImg, afterImg, { threshold = 24, minPixels = 30, maxCoverage = 0.7, pad = 0.03 } = {}) {
|
|
3790
|
+
const w = beforeImg.naturalWidth, h = beforeImg.naturalHeight;
|
|
3791
|
+
if (w !== afterImg.naturalWidth || h !== afterImg.naturalHeight) return null; // different-shaped images: don't guess
|
|
3792
|
+
const c1 = document.createElement('canvas'); c1.width = w; c1.height = h;
|
|
3793
|
+
const c2 = document.createElement('canvas'); c2.width = w; c2.height = h;
|
|
3794
|
+
c1.getContext('2d').drawImage(beforeImg, 0, 0, w, h);
|
|
3795
|
+
c2.getContext('2d').drawImage(afterImg, 0, 0, w, h);
|
|
3796
|
+
const d1 = c1.getContext('2d').getImageData(0, 0, w, h).data;
|
|
3797
|
+
const d2 = c2.getContext('2d').getImageData(0, 0, w, h).data;
|
|
3798
|
+
let minX = w, minY = h, maxX = -1, maxY = -1, diffPx = 0;
|
|
3799
|
+
for (let y = 0; y < h; y++) {
|
|
3800
|
+
for (let x = 0; x < w; x++) {
|
|
3801
|
+
const i = (y * w + x) * 4;
|
|
3802
|
+
const d = Math.abs(d1[i] - d2[i]) + Math.abs(d1[i + 1] - d2[i + 1]) + Math.abs(d1[i + 2] - d2[i + 2]);
|
|
3803
|
+
if (d > threshold) {
|
|
3804
|
+
diffPx++;
|
|
3805
|
+
if (x < minX) minX = x; if (x > maxX) maxX = x;
|
|
3806
|
+
if (y < minY) minY = y; if (y > maxY) maxY = y;
|
|
3807
|
+
}
|
|
3808
|
+
}
|
|
3809
|
+
}
|
|
3810
|
+
const total = w * h;
|
|
3811
|
+
if (maxX < 0 || diffPx < minPixels) return null; // no real difference — don't box a no-op
|
|
3812
|
+
if (diffPx / total > maxCoverage) return null; // diff too diffuse to be "a spot" — a full redesign shouldn't get a red box either
|
|
3813
|
+
const padPx = Math.round(Math.max(w, h) * pad);
|
|
3814
|
+
minX = Math.max(0, minX - padPx); minY = Math.max(0, minY - padPx);
|
|
3815
|
+
maxX = Math.min(w, maxX + padPx); maxY = Math.min(h, maxY + padPx);
|
|
3816
|
+
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
3817
|
+
}
|
|
3818
|
+
function baLoadImage(src){
|
|
3819
|
+
return new Promise((resolve, reject) => {
|
|
3820
|
+
const im = new Image();
|
|
3821
|
+
im.onload = () => resolve(im);
|
|
3822
|
+
im.onerror = () => reject(new Error(`image failed to load: ${src}`));
|
|
3823
|
+
im.src = src;
|
|
3824
|
+
});
|
|
3825
|
+
}
|
|
3826
|
+
// Crop {x,y,w,h} out of a decoded image and scale it up into a small inset — same
|
|
3827
|
+
// aspect ratio as the box, never stretched. Magnification is capped at 6x so a
|
|
3828
|
+
// near-single-pixel box doesn't blow up into mush.
|
|
3829
|
+
function baZoomDataUrl(img, box, maxW = 420, maxH = 200){
|
|
3830
|
+
const scale = Math.min(maxW / box.w, maxH / box.h, 6);
|
|
3831
|
+
const outW = Math.max(1, Math.round(box.w * scale));
|
|
3832
|
+
const outH = Math.max(1, Math.round(box.h * scale));
|
|
3833
|
+
const c = document.createElement('canvas'); c.width = outW; c.height = outH;
|
|
3834
|
+
c.getContext('2d').drawImage(img, box.x, box.y, box.w, box.h, 0, 0, outW, outH);
|
|
3835
|
+
return c.toDataURL('image/png');
|
|
3836
|
+
}
|
|
3837
|
+
// A before/after pair exists only when the AI SAYS two of its shots are the same view
|
|
3838
|
+
// before and after (task.run.compare = [beforeName, afterName]). Masa 2026-07-22: an
|
|
3839
|
+
// earlier version of this guessed the pair from file names containing "before"/"after",
|
|
3840
|
+
// which is this layer inventing a relationship it cannot know — the same mistake as the
|
|
3841
|
+
// intake AI writing the pass condition. A request for three options has no "after"; only
|
|
3842
|
+
// the agent that did the work knows which images relate, so only it can say so.
|
|
3843
|
+
//
|
|
3844
|
+
// Nothing declares `compare` yet (the engine keeps cmd/url/note/check), so no card draws
|
|
3845
|
+
// a box today. The machinery below stays wired: the day the worker can declare a pair,
|
|
3846
|
+
// it lights up with no further app change.
|
|
3847
|
+
function saShotPair(shots, declared){
|
|
3848
|
+
const list = shots ?? [];
|
|
3849
|
+
const pair = Array.isArray(declared) ? declared : null;
|
|
3850
|
+
if (!pair || pair.length !== 2) return null;
|
|
3851
|
+
const find = (n) => list.find((s) => s.name === n || String(s.url || '').endsWith(String(n)));
|
|
3852
|
+
const before = find(pair[0]), after = find(pair[1]);
|
|
3853
|
+
return before && after && before !== after ? { before, after } : null;
|
|
3854
|
+
}
|
|
3855
|
+
function saPlainPairHtml(pair){
|
|
3856
|
+
const col = (sh, label) => `<div class="ba-col"><span class="balabel">${label}</span><img class="proofimg" src="${esc(sh.url)}" alt="${esc(label)}" loading="lazy"></div>`;
|
|
3857
|
+
return `<div class="beforeafter">${col(pair.before, 'Before')}${col(pair.after, 'After')}</div>`;
|
|
3858
|
+
}
|
|
3859
|
+
// Renders the pair with the changed region boxed on both, plus a cropped enlargement.
|
|
3860
|
+
// Returns null on every fallback (not two images, load failure, no clean box, different
|
|
3861
|
+
// sizes) so the caller keeps showing the plain shots — never a guessed box.
|
|
3862
|
+
async function shotsBoxedHtml(before, after){
|
|
3863
|
+
if (!before?.url || !after?.url) return null;
|
|
3864
|
+
let bImg, aImg;
|
|
3865
|
+
try { [bImg, aImg] = await Promise.all([baLoadImage(before.url), baLoadImage(after.url)]); }
|
|
3866
|
+
catch { return null; }
|
|
3867
|
+
const box = computeDiffBox(bImg, aImg);
|
|
3868
|
+
if (!box) return null;
|
|
3869
|
+
const w = bImg.naturalWidth, h = bImg.naturalHeight;
|
|
3870
|
+
const st = `left:${(box.x / w * 100).toFixed(3)}%;top:${(box.y / h * 100).toFixed(3)}%;width:${(box.w / w * 100).toFixed(3)}%;height:${(box.h / h * 100).toFixed(3)}%`;
|
|
3871
|
+
const frame = (img, label) => `<div class="ba-col"><span class="balabel">${label}</span><div class="ba-imgwrap"><img class="proofimg" src="${img.src}" alt="${label.toLowerCase()}" loading="lazy"><div class="ba-box" style="${st}"></div></div></div>`;
|
|
3872
|
+
const zoom = (dataUrl, label) => `<div class="ba-col"><img class="ba-zoomimg" src="${dataUrl}" alt="zoomed ${label}"></div>`;
|
|
3873
|
+
return `<div class="beforeafter">${frame(bImg, 'Before')}${frame(aImg, 'After')}</div>`
|
|
3874
|
+
+ `<div class="ba-zoomhd">Zoomed on the boxed area</div>`
|
|
3875
|
+
+ `<div class="beforeafter ba-zoomrow">${zoom(baZoomDataUrl(bImg, box), 'before')}${zoom(baZoomDataUrl(aImg, box), 'after')}</div>`;
|
|
3876
|
+
}
|
|
3766
3877
|
// swap the still for the real video only on demand, one at a time
|
|
3767
3878
|
document.addEventListener('click', (e) => {
|
|
3768
3879
|
const w = e.target.closest('.proofwrap');
|
|
@@ -7992,7 +8103,7 @@ function closeSummaryPera(){ $('summaryOverlay').classList.remove('show'); }
|
|
|
7992
8103
|
// 4 archived / 5 reverted — session-local; the server round-trip is what
|
|
7993
8104
|
// settles it (see saSync's "returned to review" reset).
|
|
7994
8105
|
// ============================================================================
|
|
7995
|
-
const SA = { open: false, items: [], diffs: {}, detOpen: new Set(), focusGid: null, focusScrolled: false, focusTimer: null, cur: null };
|
|
8106
|
+
const SA = { open: false, items: [], diffs: {}, actLog: {}, shotBox: {}, detOpen: new Set(), focusGid: null, focusScrolled: false, focusTimer: null, cur: null };
|
|
7996
8107
|
window.SA = SA; // top-level const isn't a window property; external checks probe window.SA (same convention as window.state above)
|
|
7997
8108
|
const SA_SVG = {
|
|
7998
8109
|
ok: '<svg viewBox="0 0 24 24"><path d="M4 12l5 5L20 6"/></svg>',
|
|
@@ -8046,6 +8157,25 @@ function saRows(){
|
|
|
8046
8157
|
const t = ts.find((x) => x.goalId === g?.id && !x.reply);
|
|
8047
8158
|
return Array.isArray(t?.todosFinal) ? t.todosFinal : [];
|
|
8048
8159
|
})(),
|
|
8160
|
+
// Screenshots the worker took of what it built (#229). Measured 2026-07-22 on real
|
|
8161
|
+
// runs: it takes 1-3 depending on the ask — three variants as three shots, or four
|
|
8162
|
+
// variants as ONE comparison sheet. A single shot is not a lazy shot, so none of
|
|
8163
|
+
// these get thumbnailed down.
|
|
8164
|
+
//
|
|
8165
|
+
// Folded goals count: a goal folded into this one keeps its own tasks, and those
|
|
8166
|
+
// tasks carry shots. Reading only the primary goal's tasks makes real deliverables
|
|
8167
|
+
// vanish from the screen (measured: #4 folded into #3, its images disappeared).
|
|
8168
|
+
shots: (() => {
|
|
8169
|
+
const ids = [...r.goalIds, ...folded.map((f) => f.id)];
|
|
8170
|
+
return ts.filter((t) => ids.includes(t.goalId)).flatMap((t) => t.shots ?? []);
|
|
8171
|
+
})(),
|
|
8172
|
+
// The task whose implementation log the engine froze at completion (P1.5). The
|
|
8173
|
+
// lines are NOT in state — 1000 of them per task would bloat every page load — so
|
|
8174
|
+
// this is just the id to ask for when the reviewer opens Activity.
|
|
8175
|
+
logTaskId: (() => {
|
|
8176
|
+
const t = ts.find((x) => x.goalId === g?.id && !x.reply && x.activityLog);
|
|
8177
|
+
return t?.id ?? null;
|
|
8178
|
+
})(),
|
|
8049
8179
|
// P1 (Masa 2026-07-21): the AI's own final report, verbatim and untrimmed — the
|
|
8050
8180
|
// card renders it as Markdown and interprets nothing. This is the deliverable for
|
|
8051
8181
|
// every ask that has no running thing to open ("research this", "explain that"),
|
|
@@ -8068,9 +8198,7 @@ function saRows(){
|
|
|
8068
8198
|
testResult: (typeof (g?.reviewSummary?.tests) === 'string')
|
|
8069
8199
|
? { ran: true, ok: true, passed: parseInt(g.reviewSummary.tests, 10) || 0, failed: 0, label: g.reviewSummary.tests }
|
|
8070
8200
|
: r.testResult,
|
|
8071
|
-
answer: g?.reviewSummary?.answer ?? null,
|
|
8072
8201
|
activity: Array.isArray(g?.reviewSummary?.activity) ? g.reviewSummary.activity : null,
|
|
8073
|
-
discussion: Array.isArray(g?.reviewSummary?.discussion) ? g.reviewSummary.discussion : null,
|
|
8074
8202
|
changedDesc: g?.reviewSummary?.changed ?? null,
|
|
8075
8203
|
metaFiles: Array.isArray(g?.reviewSummary?.files) ? g.reviewSummary.files : null,
|
|
8076
8204
|
metaAdd: g?.reviewSummary?.a ?? null, metaDel: g?.reviewSummary?.d ?? null,
|
|
@@ -8085,7 +8213,6 @@ function saRows(){
|
|
|
8085
8213
|
recordingUrl: pt?.proof?.mp4 ? withKey(`/proof/${pt.id}/${pt.proof.mp4}`) : (typeof g?.reviewSummary?.recording === 'string' ? g.reviewSummary.recording : null),
|
|
8086
8214
|
// Local URL (spec §5): a running localhost the reviewer can open ("Local localhost:4809 ↗").
|
|
8087
8215
|
// Falls back to the existing "Local run ↗" (runlocal) button when absent.
|
|
8088
|
-
localUrl: g?.reviewSummary?.localUrl ?? null,
|
|
8089
8216
|
// Client-side diff fallback: the git-backed /diff endpoint is null for seeded/
|
|
8090
8217
|
// offline goals, so "What changed" never renders. A unified-diff string stored on
|
|
8091
8218
|
// the goal (or any of its tasks) lets saGhDiffHtml render locally with no server.
|
|
@@ -8111,35 +8238,30 @@ function saRows(){
|
|
|
8111
8238
|
// (P1, Masa 2026-07-21) saMetaRowHtml() was deleted here — the pre-redesign meta row,
|
|
8112
8239
|
// uncalled since the review-card redesign. Its "Run locally" chip only ever opened a
|
|
8113
8240
|
// note saying no run command was wired; the card's Open it chip now starts the real one.
|
|
8114
|
-
//
|
|
8115
|
-
//
|
|
8116
|
-
//
|
|
8117
|
-
// the AI's own read on the request as "your ask" and it wasn't recognizable).
|
|
8118
|
-
function saContextHtml(it, gk){
|
|
8119
|
-
const goalTxt = it.goalText;
|
|
8120
|
-
const summary = goalTxt
|
|
8121
|
-
? `"${esc(goalTxt.length > 60 ? `${goalTxt.slice(0, 60)}…` : goalTxt)}"`
|
|
8122
|
-
: 'No verbatim ask on record for this goal.';
|
|
8123
|
-
const split = it.siblings
|
|
8124
|
-
? `<div class="lg-splitnote"><div class="hd">Split into ${it.siblings.length} goals — nothing else was dropped</div><ul>${it.siblings.map((s) => `<li class="${s.cur ? 'cur' : ''}">${s.no != null ? `#${esc(s.no)} ` : ''}${esc(s.title)}${s.cur ? ' <b>(this one)</b>' : ''}</li>`).join('')}</ul></div>`
|
|
8125
|
-
: '';
|
|
8126
|
-
const did = it.did?.length ? `<div class="lg-bullethd">What we did</div><ul class="lg-bullets">${it.did.map((s) => `<li>${esc(s)}</li>`).join('')}</ul>` : '';
|
|
8127
|
-
const look = it.look?.length ? `<div class="lg-bullethd">What to look at</div><ul class="lg-bullets">${it.look.map((s) => `<li>${esc(s)}</li>`).join('')}</ul>` : '';
|
|
8128
|
-
const inner = (split || did || look) ? `${split}${did}${look}` : '<div class="lg-notfound">Nothing else on record for this goal.</div>';
|
|
8129
|
-
const open = SA.detOpen.has(`${gk}:ctx`) ? ' open' : '';
|
|
8130
|
-
return `<details class="lg-goal" data-detkey="${gk}:ctx"${open}><summary><span class="k">Context</span><span class="v">${summary}</span><i class="gtoggle">›</i></summary><div class="lg-context">${inner}</div></details>`;
|
|
8131
|
-
}
|
|
8132
|
-
// (P1, Masa 2026-07-21) saEvidenceHtml() was deleted here. It rendered the four
|
|
8133
|
-
// "evidence hero" shapes for the pre-redesign card — including the 'missing' one that
|
|
8134
|
-
// told the reviewer "No capture — this touched the UI and should have one". Nothing has
|
|
8135
|
-
// called it since the review-card redesign, and its premise (we owe you a screenshot)
|
|
8136
|
-
// is exactly what P1 removes. The report the AI wrote, plus Open it, is the evidence.
|
|
8241
|
+
// (saContextHtml() was deleted here — Masa 2026-07-22. The pre-redesign Context fold:
|
|
8242
|
+
// verbatim quote, 'What we did' / 'What to look at' bullets, sibling split note. Nothing
|
|
8243
|
+
// has called it since the review-card redesign; Request and Result carry that ground now.)
|
|
8137
8244
|
// Unified-diff → GitHub two-gutter model (REVIEW-CARD-SPEC §7 "GitHub diff = Bright"):
|
|
8138
8245
|
// walk the @@ hunk headers to recover the old/new line numbers, emit one row per line
|
|
8139
8246
|
// {o:oldNo|null, n:newNo|null, t:'add'|'del'|'ctx', c:text}. Word-level highlight is
|
|
8140
8247
|
// NOT fabricated from a raw diff (the product never invents evidence) — the Bright
|
|
8141
8248
|
// line+gutter tint carries the change and .gh-hi simply stays absent. Pure, so it is
|
|
8142
8249
|
// extracted and unit-tested in engine/test/review-card-diff.test.mjs.
|
|
8250
|
+
// How many body rows the diff really has, ignoring the display cap. A big change used
|
|
8251
|
+
// to be cut at 80 rows with nothing said (SPEC §7 asks for "…+N 行 · GitHub で開く ↗"
|
|
8252
|
+
// precisely so a reviewer is never shown a slice that looks like the whole thing).
|
|
8253
|
+
// Same skip rules as saUnifiedToGh below — headers and @@ markers are not rows.
|
|
8254
|
+
function saDiffRowCount(diff){
|
|
8255
|
+
let n = 0;
|
|
8256
|
+
for (const raw of String(diff || '').split('\n')) {
|
|
8257
|
+
if (raw === '') continue;
|
|
8258
|
+
if (/^(diff --git|index |--- |\+\+\+ |new file|deleted file|old mode|new mode|similarity|rename |Binary )/.test(raw)) continue;
|
|
8259
|
+
if (/^@@ -\d+(?:,\d+)? \+\d+(?:,\d+)? @@/.test(raw)) continue;
|
|
8260
|
+
if (raw.startsWith('\\')) continue;
|
|
8261
|
+
n++;
|
|
8262
|
+
}
|
|
8263
|
+
return n;
|
|
8264
|
+
}
|
|
8143
8265
|
function saUnifiedToGh(diff, max){
|
|
8144
8266
|
const cap = max || 80; const out = []; let o = 0, n = 0;
|
|
8145
8267
|
for (const raw of String(diff || '').split('\n')) {
|
|
@@ -8155,10 +8277,13 @@ function saUnifiedToGh(diff, max){
|
|
|
8155
8277
|
}
|
|
8156
8278
|
return out;
|
|
8157
8279
|
}
|
|
8158
|
-
function saGhDiffHtml(diff){
|
|
8280
|
+
function saGhDiffHtml(diff, pr){
|
|
8159
8281
|
const rows = saUnifiedToGh(diff);
|
|
8160
8282
|
if (!rows.length) return '';
|
|
8161
|
-
|
|
8283
|
+
// Say what was left out. Before this, a 5000-line change rendered its first 80 rows
|
|
8284
|
+
// and stopped — a quiet lie, and the one thing SPEC §7 names as forbidden.
|
|
8285
|
+
const more = Math.max(0, saDiffRowCount(diff) - rows.length);
|
|
8286
|
+
return saGhDiffRowsHtml(rows, more, pr || '');
|
|
8162
8287
|
}
|
|
8163
8288
|
// One Activity transcript row (spec §3): You / tool (mono, bold name) / res (green) /
|
|
8164
8289
|
// Claude. Mirrors the flagship artifact's tx mapping so the pulldown pixel-matches.
|
|
@@ -8168,6 +8293,18 @@ function saTxRowHtml(e){
|
|
|
8168
8293
|
if (e[0] === 'user') return `<div class="txr you"><span class="w">You</span><span class="m">${esc(e[1])}</span></div>`;
|
|
8169
8294
|
return `<div class="txr cl"><span class="w">Claude</span><span class="m">${esc(e[1])}</span></div>`;
|
|
8170
8295
|
}
|
|
8296
|
+
// One line of the frozen implementation log. The worker's own tool calls come through
|
|
8297
|
+
// as "Read app/index.html" / "Bash npm test"; the Manager's own progress notes are
|
|
8298
|
+
// prefixed with ✎ by sendProcessAct() and read as commentary, so they take the quieter
|
|
8299
|
+
// row. Nothing is parsed beyond that split — the line is shown as it was written.
|
|
8300
|
+
function saLogRowHtml(line){
|
|
8301
|
+
const s = String(line ?? '');
|
|
8302
|
+
if (s.startsWith('✎')) return `<div class="txr note"><span class="w"></span><span class="m">${esc(s.replace(/^✎\s*/, ''))}</span></div>`;
|
|
8303
|
+
const m = s.match(/^([A-Z][A-Za-z]*)\s+([\s\S]*)$/);
|
|
8304
|
+
return m
|
|
8305
|
+
? `<div class="txr tool"><span class="w">tool</span><span class="m"><b>${esc(m[1])}</b> ${esc(m[2])}</span></div>`
|
|
8306
|
+
: `<div class="txr cl"><span class="w"></span><span class="m">${esc(s)}</span></div>`;
|
|
8307
|
+
}
|
|
8171
8308
|
// Structured GitHub diff (REVIEW-CARD-SPEC §7): renders {o,n,t,c,h} rows with a
|
|
8172
8309
|
// word-level highlight (h) and, for a huge change, a "…+N 行 · GitHub で開く ↗"
|
|
8173
8310
|
// footer (only linking to GitHub when a PR exists). Mirrors the flagship artifact's
|
|
@@ -8207,7 +8344,15 @@ function saWorkflowHtml(it){
|
|
|
8207
8344
|
// P1 (Masa 2026-07-21): "Proof captured □" is gone. We no longer go take photos of
|
|
8208
8345
|
// the work, so an empty box there was scoring the goal against a step that isn't
|
|
8209
8346
|
// part of the process any more — a permanent unchecked box nobody could ever check.
|
|
8210
|
-
|
|
8347
|
+
// "Deploy" is gone too (Masa 2026-07-22): nothing in the product observes a deploy,
|
|
8348
|
+
// so that box could never be ticked — the same permanent-□ problem as Proof captured.
|
|
8349
|
+
// Merge stays because it IS observed: the PR poller flips the goal to done on merge.
|
|
8350
|
+
//
|
|
8351
|
+
// (PRD §5.5-A/15 asks for the USER'S OWN pipeline — PRD→design→implement→check→PR→
|
|
8352
|
+
// merge→deploy, varying per person. There is no place to record such a pipeline yet,
|
|
8353
|
+
// so inventing one here would be this layer guessing someone's process. Left as a
|
|
8354
|
+
// decision, not faked.)
|
|
8355
|
+
const steps = [['Implemented', 'ok'], ['Tested', ran ? 'ok' : 'todo'], ['In review', 'now'], ['Merge', 'todo']];
|
|
8211
8356
|
return '<div class="rwf">' + steps.map(([s, st]) => `<div class="wf-s ${st}"><span class="mk">${st === 'ok' ? '✓' : st === 'now' ? '→' : '□'}</span>${s}</div>`).join('') + '</div>';
|
|
8212
8357
|
}
|
|
8213
8358
|
function saItemHtml(it, i){
|
|
@@ -8250,9 +8395,9 @@ function saItemHtml(it, i){
|
|
|
8250
8395
|
const kind = saEvidenceKind(filePaths, hasProofImg, gtr);
|
|
8251
8396
|
const gk = it.goalId;
|
|
8252
8397
|
const rs = it.goal?.reviewSummary || {};
|
|
8253
|
-
//
|
|
8254
|
-
//
|
|
8255
|
-
const isConv =
|
|
8398
|
+
// A conversational task (no code change) has no Proof / What changed to show. Its
|
|
8399
|
+
// Result IS the answer (P1) — the old separate Answer section is gone.
|
|
8400
|
+
const isConv = !it.report && !filePaths.length;
|
|
8256
8401
|
// ── meta (spec §5) — fully data-driven / hide-empty: each chip renders only when its
|
|
8257
8402
|
// data exists (no PR → no PR chip · no tests → no Tests · no file totals → no Files ·
|
|
8258
8403
|
// no recording → no Verify). PR = bold green, no underline (a real <a href>). ──
|
|
@@ -8269,10 +8414,9 @@ function saItemHtml(it, i){
|
|
|
8269
8414
|
if (testsLabel) chips.push(`<span class="mx"><span class="mk">Tests</span> <b class="ok">${esc(testsLabel)}</b></span>`);
|
|
8270
8415
|
// Files: hide-empty — only with changed files AND totals (a bare-count chip reads odd).
|
|
8271
8416
|
if (nf && fAdd) chips.push(`<span class="mx"><span class="mk">Files</span> <b>${nf}</b> <span class="mmut">· <i class="a">${esc(fAdd)}</i> <i class="d">${esc(fDel)}</i></span></span>`);
|
|
8272
|
-
// Local
|
|
8273
|
-
//
|
|
8274
|
-
|
|
8275
|
-
chips.push(`<a class="mx mlink"${isUrl ? ` href="${esc(lu)}" target="_blank" rel="noopener"` : ' style="cursor:pointer"'} title="Open the running app"><span class="mk">Local</span> <b>${esc(lu.replace(/^https?:\/\//, ''))}</b>${arw}</a>`); }
|
|
8417
|
+
// (The Local chip was deleted here — Masa 2026-07-22. It read reviewSummary.localUrl,
|
|
8418
|
+
// a key parseReviewSummary never emits, so it only ever appeared in demo data; and the
|
|
8419
|
+
// real thing it promised is the Open it section.)
|
|
8276
8420
|
// (Open it is NOT a meta chip — Masa 2026-07-22. It was saying the same thing as the
|
|
8277
8421
|
// Open it section below, in the PR chip's vocabulary, so the card spoke two languages
|
|
8278
8422
|
// about one thing. It lives in the body now, in the same typography as every other
|
|
@@ -8333,6 +8477,21 @@ function saItemHtml(it, i){
|
|
|
8333
8477
|
// change explains itself here, and an image the AI pasted shows up here as an image.
|
|
8334
8478
|
// We interpret nothing; mdLite only turns Markdown into the shapes it already means. ──
|
|
8335
8479
|
const reportBody = it.report ? `<div class="rreport">${mdLite(it.report, { images: false })}</div>` : '';
|
|
8480
|
+
// ── Screenshots (P2, Masa 2026-07-22): the pictures the AI took of its own result.
|
|
8481
|
+
// Full width, at their own aspect — a four-variant comparison sheet is unreadable
|
|
8482
|
+
// shrunk into a thumbnail, and that is the shape the AI actually produces. The file
|
|
8483
|
+
// name it chose is the caption (it names them variant-1-linear.png / compare.png), so
|
|
8484
|
+
// it labels its own work; we add nothing. ──
|
|
8485
|
+
// A before/after pair is two shots the worker named as such — it decides whether the
|
|
8486
|
+
// change had a "before" worth shooting, we only recognise the naming. Anything else is
|
|
8487
|
+
// shown as the plain list.
|
|
8488
|
+
const shotPair = saShotPair(it.shots, it.run?.compare);
|
|
8489
|
+
const boxed = shotPair ? SA.shotBox[it.goalId] : null;
|
|
8490
|
+
const shotsBody = shotPair
|
|
8491
|
+
? `<div class="rshotpair" data-shotpair="${it.goalId}">${typeof boxed === 'string' ? boxed : saPlainPairHtml(shotPair)}</div>`
|
|
8492
|
+
: it.shots?.length
|
|
8493
|
+
? it.shots.map((sh) => `<figure class="rshot"><img src="${esc(sh.url)}" alt="${esc(sh.name || 'screenshot')}" loading="lazy"><figcaption>${esc(String(sh.name || '').replace(/\.[a-z]+$/i, ''))}</figcaption></figure>`).join('')
|
|
8494
|
+
: '';
|
|
8336
8495
|
// ── Proof (spec §6/§11): before/after (identical size) when a real image exists; a
|
|
8337
8496
|
// run-log for logic-only tests; dropped entirely for a conversational task. ──
|
|
8338
8497
|
let proofInner = '';
|
|
@@ -8355,7 +8514,7 @@ function saItemHtml(it, i){
|
|
|
8355
8514
|
: (clientDiff && filePaths.length) ? `<div class="rfiles">${filePaths.map((p) => esc(p)).join('<br>')}</div>` : '';
|
|
8356
8515
|
const diffHtml = it.ghRows
|
|
8357
8516
|
? saGhDiffRowsHtml(it.ghRows, it.diffTotal || 0, it.pr || '')
|
|
8358
|
-
: hasGitDiff ? saGhDiffHtml(d.diff) : clientDiff ? saGhDiffHtml(clientDiff) : '';
|
|
8517
|
+
: hasGitDiff ? saGhDiffHtml(d.diff, it.pr) : clientDiff ? saGhDiffHtml(clientDiff, it.pr) : '';
|
|
8359
8518
|
const changedBody = isConv ? '' : (changedDescHtml + filesListHtml + diffHtml);
|
|
8360
8519
|
// ── sensitive-area note (real: classifyChangeRisk forced the pause) ──
|
|
8361
8520
|
const risk = it.risk;
|
|
@@ -8373,17 +8532,30 @@ function saItemHtml(it, i){
|
|
|
8373
8532
|
const txArr = (it.activity && it.activity.length)
|
|
8374
8533
|
? it.activity.map((e) => saTxRowHtml(e))
|
|
8375
8534
|
: ((!reportBody && it.did?.length) ? it.did.map((s) => saTxRowHtml(['claude', s])) : []);
|
|
8376
|
-
|
|
8377
|
-
|
|
8378
|
-
|
|
8379
|
-
|
|
8535
|
+
// (Discussion was deleted here — Masa 2026-07-22. It read reviewSummary.discussion,
|
|
8536
|
+
// which parseReviewSummary never emits, so it never rendered on a real goal; replies
|
|
8537
|
+
// already live in the card's own reply box.)
|
|
8538
|
+
const threadHtml = '';
|
|
8539
|
+
// ── Activity = the implementation log (P1.5, Masa 2026-07-22): what the AI actually
|
|
8540
|
+
// did, read AFTER it finished. Fetched only when the section is opened — a task's log
|
|
8541
|
+
// runs to ~500KB and most cards are never expanded — so the closed state is one line
|
|
8542
|
+
// and the lines arrive on the first open (same "only when asked" shape as Open it).
|
|
8543
|
+
// Once fetched they live in SA.actLog so an SSE rebuild doesn't wipe them.
|
|
8544
|
+
const logLines = it.logTaskId != null ? SA.actLog[it.logTaskId] : null;
|
|
8545
|
+
const logHtml = Array.isArray(logLines)
|
|
8546
|
+
? (logLines.length
|
|
8547
|
+
? logLines.map((l) => saLogRowHtml(l)).join('')
|
|
8548
|
+
: '<div class="txr res"><span class="w"></span><span class="m">Nothing was logged for this one.</span></div>')
|
|
8549
|
+
: '<div class="txr res"><span class="w"></span><span class="m">Loading…</span></div>';
|
|
8380
8550
|
const actOpen = SA.detOpen.has(`${gk}:act`) ? ' open' : '';
|
|
8381
8551
|
const txFirst = txArr[0] || '<div class="txr"><span class="w"></span><span class="m">Activity</span></div>';
|
|
8382
8552
|
const txRest = txArr.slice(1).join('');
|
|
8383
8553
|
const discInner = threadHtml ? `<div class="rsub">Discussion</div><div class="rthread">${threadHtml}</div>` : '';
|
|
8384
8554
|
const activitySec = (txArr.length || threadHtml)
|
|
8385
8555
|
? `<section class="rsec ract"><div class="rsec-h">Activity</div><details class="lacc" data-detkey="${gk}:act"${actOpen}><summary><i class="ltog">›</i><div class="rtx lfirst">${txFirst}</div></summary><div class="lbody">${txRest ? `<div class="rtx">${txRest}</div>` : ''}${discInner}</div></details></section>`
|
|
8386
|
-
:
|
|
8556
|
+
: it.logTaskId != null
|
|
8557
|
+
? `<section class="rsec ract"><div class="rsec-h">Activity</div><details class="lacc" data-detkey="${gk}:act" data-actlog="${it.logTaskId}"${actOpen}><summary><i class="ltog">›</i><div class="rtx lfirst">${saTxRowHtml(['user', it.goalText || it.title])}</div></summary><div class="lbody"><div class="rtx" data-actbody="${it.logTaskId}">${logHtml}</div>${discInner}</div></details></section>`
|
|
8558
|
+
: '';
|
|
8387
8559
|
const S = (label, body) => body ? `<section class="rsec"><div class="rsec-h">${label}</div>${body}</section>` : '';
|
|
8388
8560
|
return `<div class="sa-item lg redesign${it.goalId === SA.cur ? ' cur' : ''}" data-gid="${it.goalId}">
|
|
8389
8561
|
<div class="rhead"><span class="rtag">${it.no != null ? `#${it.no}` : ''}</span><span class="rst"><span class="rdot"></span>In review</span></div>
|
|
@@ -8394,11 +8566,10 @@ function saItemHtml(it, i){
|
|
|
8394
8566
|
${S('Also carries', carriesBody)}
|
|
8395
8567
|
${S('What to check', checkBody)}
|
|
8396
8568
|
${S('Result', reportBody)}
|
|
8569
|
+
${S('Screenshots', shotsBody)}
|
|
8397
8570
|
${S('Steps', stepsBody)}
|
|
8398
|
-
${S('Answer', it.answer ? `<p class="rbody">${esc(it.answer)}</p>` : '')}
|
|
8399
8571
|
${S('Proof', proofInner)}
|
|
8400
8572
|
${S('What changed', changedBody)}
|
|
8401
|
-
${S('Not done', it.risk?.summary ? `<p class="rbody rrisk">${esc(it.risk.summary)}</p>` : '')}
|
|
8402
8573
|
${riskHtml}
|
|
8403
8574
|
${activitySec}
|
|
8404
8575
|
${S('Workflow', saWorkflowHtml(it))}
|
|
@@ -8413,6 +8584,40 @@ function saScrollCardToTop(el){
|
|
|
8413
8584
|
if (!body || !el) return;
|
|
8414
8585
|
body.scrollTop += el.getBoundingClientRect().top - body.getBoundingClientRect().top;
|
|
8415
8586
|
}
|
|
8587
|
+
// Fetch one task's frozen implementation log and paint it into the open card. Cached in
|
|
8588
|
+
// SA.actLog (including an empty result) so a card can be opened and closed without
|
|
8589
|
+
// asking twice. A failure writes an honest line rather than an empty box — the log
|
|
8590
|
+
// either exists or it doesn't, and we say which.
|
|
8591
|
+
async function saLoadActivityLog(taskId){
|
|
8592
|
+
if (!Number.isFinite(taskId) || SA.actLog[taskId] !== undefined) return;
|
|
8593
|
+
SA.actLog[taskId] = null; // in flight — keeps a second toggle from re-fetching
|
|
8594
|
+
let lines = [];
|
|
8595
|
+
try {
|
|
8596
|
+
const r = await fetch(withKey(`/api/tasks/${taskId}/activity`));
|
|
8597
|
+
lines = r.ok ? ((await r.json()).lines ?? []) : [];
|
|
8598
|
+
} catch { lines = []; }
|
|
8599
|
+
SA.actLog[taskId] = lines;
|
|
8600
|
+
const body = document.querySelector(`#seeall [data-actbody="${taskId}"]`);
|
|
8601
|
+
if (!body) return;
|
|
8602
|
+
body.innerHTML = lines.length
|
|
8603
|
+
? lines.map((l) => saLogRowHtml(l)).join('')
|
|
8604
|
+
: '<div class="txr res"><span class="w"></span><span class="m">Nothing was logged for this one.</span></div>';
|
|
8605
|
+
}
|
|
8606
|
+
// After first paint, compute the red box for any card showing a before/after pair and
|
|
8607
|
+
// patch that card's node directly — a full re-render here would close every open
|
|
8608
|
+
// pulldown just to draw a box. Cached per goal (null included) so it runs once.
|
|
8609
|
+
async function saEnhanceShotBoxes(){
|
|
8610
|
+
const targets = SA.items.filter((it) => saShotPair(it.shots, it.run?.compare) && SA.shotBox[it.goalId] === undefined);
|
|
8611
|
+
await Promise.all(targets.map(async (it) => {
|
|
8612
|
+
const pair = saShotPair(it.shots, it.run?.compare);
|
|
8613
|
+
let html = null;
|
|
8614
|
+
try { html = await shotsBoxedHtml(pair.before, pair.after); } catch { html = null; }
|
|
8615
|
+
SA.shotBox[it.goalId] = html;
|
|
8616
|
+
if (!html || !SA.open) return;
|
|
8617
|
+
const host = document.querySelector(`#seeall [data-shotpair="${it.goalId}"]`);
|
|
8618
|
+
if (host) host.innerHTML = html;
|
|
8619
|
+
}));
|
|
8620
|
+
}
|
|
8416
8621
|
function saRender(){
|
|
8417
8622
|
const S = $('seeall'); if (!S) return;
|
|
8418
8623
|
const pending = SA.items.filter((x) => x.st === 0);
|
|
@@ -8429,7 +8634,14 @@ function saRender(){
|
|
|
8429
8634
|
det.addEventListener('toggle', () => {
|
|
8430
8635
|
const k = det.dataset.detkey;
|
|
8431
8636
|
if (det.open) SA.detOpen.add(k); else SA.detOpen.delete(k);
|
|
8637
|
+
// Opening Activity is what fetches the implementation log (Masa 2026-07-22:
|
|
8638
|
+
// "開いた時だけ取りに行く"). Closed cards cost nothing; a reopened one is served
|
|
8639
|
+
// from SA.actLog.
|
|
8640
|
+
if (det.open && det.dataset.actlog) saLoadActivityLog(Number(det.dataset.actlog));
|
|
8432
8641
|
});
|
|
8642
|
+
// A card re-rendered while its Activity was already open (SSE tick) keeps the log
|
|
8643
|
+
// it had — the fetch below no-ops once SA.actLog holds the answer.
|
|
8644
|
+
if (det.open && det.dataset.actlog) saLoadActivityLog(Number(det.dataset.actlog));
|
|
8433
8645
|
}
|
|
8434
8646
|
// "ないものは載せない" (Masa): a Proof <img> whose file is missing/broken must never
|
|
8435
8647
|
// show a broken-image placeholder. proofImg is only a stored filename, so a file that
|
|
@@ -8511,6 +8723,7 @@ function saOpen(focusGoalId){
|
|
|
8511
8723
|
if (el) saScrollCardToTop(el);
|
|
8512
8724
|
});
|
|
8513
8725
|
saFetchDiffs();
|
|
8726
|
+
saEnhanceShotBoxes();
|
|
8514
8727
|
}
|
|
8515
8728
|
function saClose(){ SA.open = false; SA.focusGid = null; if (SA.focusTimer) { clearTimeout(SA.focusTimer); SA.focusTimer = null; } $('seeall').classList.remove('open'); }
|
|
8516
8729
|
// Continuous review (Masa 2026-07-08): move a persistent "current" card with
|
package/engine/lib.mjs
CHANGED
|
@@ -534,6 +534,96 @@ export function parseStreamEvents(line) {
|
|
|
534
534
|
return out;
|
|
535
535
|
}
|
|
536
536
|
|
|
537
|
+
// ---- CODEX BRIDGE ----------------------------------------------------------
|
|
538
|
+
// The same three wires the product is built out of — the board (the agent's own
|
|
539
|
+
// to-dos), the implementation log (what it actually did), and the thread it can
|
|
540
|
+
// be talked to again in — read from `codex exec --json` instead of `claude -p
|
|
541
|
+
// --output-format stream-json`. Everything above this line stays agent-agnostic:
|
|
542
|
+
// the bridge's whole job is to speak Codex's event vocabulary into the one this
|
|
543
|
+
// engine already has, so nothing downstream branches on which agent ran.
|
|
544
|
+
//
|
|
545
|
+
// The shapes below are transcribed from a real `codex exec --json` run
|
|
546
|
+
// (codex-cli 0.142.4) — see engine/test/codex-bridge.test.mjs, whose fixtures
|
|
547
|
+
// are that run's stdout verbatim rather than a schema we imagined.
|
|
548
|
+
export function parseCodexTodos(items) {
|
|
549
|
+
return (Array.isArray(items) ? items : [])
|
|
550
|
+
.map((i) => ({ content: String(i?.text ?? '').trim(), status: i?.completed ? 'completed' : 'pending' }))
|
|
551
|
+
.filter((t) => t.content);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Codex reports usage with its own key names AND its own arithmetic: its
|
|
555
|
+
// `input_tokens` INCLUDES the cached ones, where claude reports cache reads as a
|
|
556
|
+
// separate bucket that input_tokens excludes. Adding both as-is counted the
|
|
557
|
+
// cached tokens twice, so the ledger showed a Codex run as far more expensive
|
|
558
|
+
// than it was (measured: ~92k of a ~108k turn was cache). Subtract, so a number
|
|
559
|
+
// from either agent means the same thing.
|
|
560
|
+
export function parseCodexUsage(usage) {
|
|
561
|
+
if (!usage) return null;
|
|
562
|
+
const total = usage.input_tokens ?? usage.inputTokens ?? 0;
|
|
563
|
+
const cached = usage.cached_input_tokens ?? usage.cache_read_input_tokens ?? 0;
|
|
564
|
+
return {
|
|
565
|
+
input_tokens: Math.max(0, total - cached),
|
|
566
|
+
output_tokens: usage.output_tokens ?? usage.outputTokens ?? 0,
|
|
567
|
+
cache_creation_input_tokens: usage.cache_creation_input_tokens ?? 0,
|
|
568
|
+
cache_read_input_tokens: cached,
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Parse one line of `codex exec --json` output into the same event vocabulary
|
|
573
|
+
// parseStreamEvents returns ('session' | 'todos' | 'activity' | 'message' |
|
|
574
|
+
// 'usage'). 'message' rather than 'result' because Codex emits an agent_message
|
|
575
|
+
// per narration step, not once at the end — the caller keeps the last one.
|
|
576
|
+
export function parseCodexEvents(line) {
|
|
577
|
+
let ev;
|
|
578
|
+
try { ev = JSON.parse(line); } catch { return []; }
|
|
579
|
+
if (!ev || typeof ev !== 'object') return [];
|
|
580
|
+
if (ev.type === 'thread.started' && ev.thread_id) return [{ kind: 'session', id: String(ev.thread_id) }];
|
|
581
|
+
if (ev.type === 'turn.completed') return [{ kind: 'usage', usage: parseCodexUsage(ev.usage) }];
|
|
582
|
+
if (ev.type === 'error' && ev.message) return [{ kind: 'error', text: String(ev.message) }];
|
|
583
|
+
if (!['item.started', 'item.updated', 'item.completed'].includes(ev.type)) return [];
|
|
584
|
+
const item = ev.item ?? {};
|
|
585
|
+
if (item.type === 'todo_list') {
|
|
586
|
+
const todos = parseCodexTodos(item.items);
|
|
587
|
+
return todos.length ? [{ kind: 'todos', todos }] : [];
|
|
588
|
+
}
|
|
589
|
+
if (item.type === 'agent_message' && ev.type === 'item.completed' && item.text) {
|
|
590
|
+
return [{ kind: 'message', text: String(item.text) }];
|
|
591
|
+
}
|
|
592
|
+
// Commands are announced when they start (the log should move while the
|
|
593
|
+
// worker works, not after) and only mentioned again when they fail.
|
|
594
|
+
if (item.type === 'command_execution' && item.command) {
|
|
595
|
+
if (ev.type === 'item.started') return [{ kind: 'activity', text: `Run ${String(item.command)}`.slice(0, 140) }];
|
|
596
|
+
if (ev.type === 'item.completed' && item.exit_code) {
|
|
597
|
+
return [{ kind: 'activity', text: `Run ${String(item.command)} (exit ${item.exit_code})`.slice(0, 140) }];
|
|
598
|
+
}
|
|
599
|
+
return [];
|
|
600
|
+
}
|
|
601
|
+
// File edits are the bulk of what a worker does and were entirely missing from
|
|
602
|
+
// the log on Codex — the run read as "a few commands and then, somehow, a PR".
|
|
603
|
+
if (item.type === 'file_change' && ev.type === 'item.completed') {
|
|
604
|
+
const verb = { add: 'Write', delete: 'Delete' };
|
|
605
|
+
return (Array.isArray(item.changes) ? item.changes : [])
|
|
606
|
+
.filter((c) => c?.path)
|
|
607
|
+
.slice(0, 8)
|
|
608
|
+
.map((c) => ({ kind: 'activity', text: `${verb[c.kind] ?? 'Edit'} ${String(c.path)}`.slice(0, 140) }));
|
|
609
|
+
}
|
|
610
|
+
if (item.type === 'error' && item.message && ev.type === 'item.completed') {
|
|
611
|
+
return [{ kind: 'error', text: String(item.message) }];
|
|
612
|
+
}
|
|
613
|
+
return [];
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Argument list for one `codex exec` run. `resume` continues an existing thread
|
|
617
|
+
// (the id from a previous run's thread.started) — that subcommand takes neither
|
|
618
|
+
// --cd nor --sandbox, so the working directory comes from the spawn's cwd and
|
|
619
|
+
// the sandbox from -c sandbox_mode. Verified against codex-cli 0.142.4.
|
|
620
|
+
export function buildCodexArgs({ model, effort, sandbox, cwd, resume } = {}) {
|
|
621
|
+
const common = ['--json', '-m', model, '-c', `model_reasoning_effort="${effort}"`, '--skip-git-repo-check'];
|
|
622
|
+
return resume
|
|
623
|
+
? ['exec', 'resume', String(resume), ...common, '-c', `sandbox_mode="${sandbox}"`, '-']
|
|
624
|
+
: ['exec', ...common, '--sandbox', sandbox, '--cd', cwd, '-'];
|
|
625
|
+
}
|
|
626
|
+
|
|
537
627
|
// Parse the planner's reply into { entry, tasks }. Accepts either a JSON
|
|
538
628
|
// object {entry, tasks:[...]} or a bare JSON array anywhere in the text;
|
|
539
629
|
// falls back to a single task made from the raw request. Each task may carry
|
|
@@ -2219,6 +2309,15 @@ export function workerPrompt(task, goal, lastFailure, agentName = 'claude-code')
|
|
|
2219
2309
|
lastFailure ? `\nPREVIOUS ATTEMPT FAILED EXTERNAL VERIFICATION: ${lastFailure}\nYour previous changes are on disk. Diagnose and fix.` : '',
|
|
2220
2310
|
'',
|
|
2221
2311
|
'RULES:',
|
|
2312
|
+
// The board is a window onto the agent's own plan — it holds nothing this
|
|
2313
|
+
// layer invented. That only works if the agent keeps a plan where we can see
|
|
2314
|
+
// it. Claude Code reaches for TodoWrite on its own; Codex has the same list
|
|
2315
|
+
// but skips it on anything small (measured 2026-07-22: gpt-5.4-mini emitted
|
|
2316
|
+
// no todo_list even when the REQUEST said "in three steps" — it does emit one
|
|
2317
|
+
// when told to track the work in its to-do list). So ask, once, for every
|
|
2318
|
+
// agent — the instruction is about how to work, never about what the request
|
|
2319
|
+
// is, so no request-type branching enters here.
|
|
2320
|
+
'- 作業の手順は最初に自分の Todo リスト(Claude Code なら TodoWrite、Codex なら todo list)に書き出し、進むたびに更新すること。人はこの Todo を見て進み具合を知る。',
|
|
2222
2321
|
'- Edit only files inside this directory and this project. Stay on this request; do not wander into unrelated work.',
|
|
2223
2322
|
// Deliverable fidelity (replaces the old "make the smallest safe change
|
|
2224
2323
|
// instead of expanding scope"): the request, not our fear of scope, sets
|
|
@@ -2251,7 +2350,14 @@ export function workerPrompt(task, goal, lastFailure, agentName = 'claude-code')
|
|
|
2251
2350
|
// to use it — that is the worker's call, like the run declaration above.
|
|
2252
2351
|
// Telling it "screenshot UI changes" would be this layer deciding what kind
|
|
2253
2352
|
// of request it is, which is the behaviour that produced off-target proof.
|
|
2254
|
-
|
|
2353
|
+
// ...but only to an agent that can actually take one. Codex runs its shell
|
|
2354
|
+
// inside a sandbox that Chrome cannot start under (measured 2026-07-22 on
|
|
2355
|
+
// codex-cli 0.142.4: the camera fails under `workspace-write` and works only
|
|
2356
|
+
// with the sandbox off — opening network access does not help). Offering it
|
|
2357
|
+
// anyway is not a harmless extra line: the Codex worker tried four times,
|
|
2358
|
+
// then reported the failure instead of the work. Do not ask an agent for
|
|
2359
|
+
// something this machine has proven it cannot do (Masa 2026-07-22).
|
|
2360
|
+
agentName === 'codex' ? '' : `- 画面・見た目のあるものを作ったなら、自分でスクリーンショットを撮って見せてよい: \`node ${SHOT_CLI} <URL または HTMLファイル> ${SHOT_DIR}/<名前>.png [--full] [--wait-for <CSSセレクタ>]\`(headless Chrome・前面に出ない)。${SHOT_DIR}/ に置いた画像は Manager が回収して人に見せる(diff には入らない)。撮ったものは Read で自分でも見返せる。`,
|
|
2255
2361
|
`- 最後に${replyLang}で2〜4行:何を変えたか・どのファイルか・テスト結果・人間が確認すべき点。`,
|
|
2256
2362
|
].filter(Boolean).join('\n');
|
|
2257
2363
|
}
|
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, makeSigninChallenge, parseClaimResponse } from './lib.mjs';
|
|
24
|
-
import { parseStreamEvents, parsePlan, refinePlanTasks, findOverlappingGoal, canEditGoal, canDeleteGoal, shouldAskClarification, workerExitReason, isForcedStop, workerResultText, taskCountsAsComplete, resolveWantsPR, resolveGoalSource, buildGoalSummary, buildGoalProofMd, buildGoalPrBody, buildReviewSummaryPrompt, parseReviewSummary, buildReviewRuleSection, nextGoalStatus, isPrMerged, isPrApproved, reviewToDoneStatus, advanceReviewGoal, revertReviewGoal, approveGoal, dismissGoal, rejectGoal, reopenGoal, permissionModeFor, isPlanReview, nextAfterPlanApprove, GOAL_MODES, parseSkillFrontmatter, collectSkills, retestGoal, cancelRetestGoal, computeRetestOutcome, revertGoal, planRevertActions, archiveGoal, unarchiveGoal, undismissGoal, parseNumstat, truncateDiffText, sumUsage, resolveEntryUrl, parseGitLog, diffNewCommits, replayQueueLog, reconcileOrphanGoals, reorderQueue, sortQueueByPriority, TASK_PRIORITIES, validateWorkflowColumns, DEFAULT_WORKFLOW_COLUMNS, validateReviewDefinition, DEFAULT_REVIEW_DEFINITION, buildEphemeralSeedLog, buildEphemeralSeedProjects, trimTaskActivityForState, buildAskContext, WORKER_TOOLS, workerPrompt, verifyCfAccessJwt, resolveIdentity, goalVisibleTo, clampParallelLimit, canStartMore, nextRunnableTasks, goalsConflict, detectConflicts, pickFoldTarget, hasTestRelevantChanges, parseFailingTestNames, classifyTestGate, classifyVerifyGate, buildExecutionPlan, buildContextHandoffSummary, buildFailurePostmortem, appendFailureMemory, latestFailurePolicy, classifyChangeRisk, checkRunBudget, usageBudgetTokens, isRateLimited, nextResumeDelay, checkFreeTierLimit, resolveEntitlement, resolveCachedEntitlement, FREE_TIER_LIMITS, QUEUED_GOAL_STATUSES, verifyLicenseToken, shouldEmitSetupCompleted, pickAnalyticsUid, shouldEmitFreeExhausted, detectRequestLanguage, isNothingVerifiable, classifyComposerIntentHeuristic, detectPauseIntent, buildIntentPrompt, parseIntentResponse, buildBoardSnapshot, validateBoardSnapshot, boardIsEmpty, decideBoardPull, resolveConnectedAgents, pickAvailableAgent, pickUtilityAgent, utilityModel, liveTakeoverDecision, shouldOpenBrowserOnBoot, goalTaskTitle, buildGoalTask, RUN_DECL_FILE, parseRunDeclaration, SHOT_DIR, collectShotNames } from './lib.mjs';
|
|
24
|
+
import { parseStreamEvents, parseCodexEvents, buildCodexArgs, parsePlan, refinePlanTasks, findOverlappingGoal, canEditGoal, canDeleteGoal, shouldAskClarification, workerExitReason, isForcedStop, workerResultText, taskCountsAsComplete, resolveWantsPR, resolveGoalSource, buildGoalSummary, buildGoalProofMd, buildGoalPrBody, buildReviewSummaryPrompt, parseReviewSummary, buildReviewRuleSection, nextGoalStatus, isPrMerged, isPrApproved, reviewToDoneStatus, advanceReviewGoal, revertReviewGoal, approveGoal, dismissGoal, rejectGoal, reopenGoal, permissionModeFor, isPlanReview, nextAfterPlanApprove, GOAL_MODES, parseSkillFrontmatter, collectSkills, retestGoal, cancelRetestGoal, computeRetestOutcome, revertGoal, planRevertActions, archiveGoal, unarchiveGoal, undismissGoal, parseNumstat, truncateDiffText, sumUsage, resolveEntryUrl, parseGitLog, diffNewCommits, replayQueueLog, reconcileOrphanGoals, reorderQueue, sortQueueByPriority, TASK_PRIORITIES, validateWorkflowColumns, DEFAULT_WORKFLOW_COLUMNS, validateReviewDefinition, DEFAULT_REVIEW_DEFINITION, buildEphemeralSeedLog, buildEphemeralSeedProjects, trimTaskActivityForState, buildAskContext, WORKER_TOOLS, workerPrompt, verifyCfAccessJwt, resolveIdentity, goalVisibleTo, clampParallelLimit, canStartMore, nextRunnableTasks, goalsConflict, detectConflicts, pickFoldTarget, hasTestRelevantChanges, parseFailingTestNames, classifyTestGate, classifyVerifyGate, buildExecutionPlan, buildContextHandoffSummary, buildFailurePostmortem, appendFailureMemory, latestFailurePolicy, classifyChangeRisk, checkRunBudget, usageBudgetTokens, isRateLimited, nextResumeDelay, checkFreeTierLimit, resolveEntitlement, resolveCachedEntitlement, FREE_TIER_LIMITS, QUEUED_GOAL_STATUSES, verifyLicenseToken, shouldEmitSetupCompleted, pickAnalyticsUid, shouldEmitFreeExhausted, detectRequestLanguage, isNothingVerifiable, classifyComposerIntentHeuristic, detectPauseIntent, buildIntentPrompt, parseIntentResponse, buildBoardSnapshot, validateBoardSnapshot, boardIsEmpty, decideBoardPull, resolveConnectedAgents, pickAvailableAgent, pickUtilityAgent, utilityModel, liveTakeoverDecision, shouldOpenBrowserOnBoot, goalTaskTitle, buildGoalTask, RUN_DECL_FILE, parseRunDeclaration, SHOT_DIR, collectShotNames } from './lib.mjs';
|
|
25
25
|
import { createSerialQueue } from './lib.mjs';
|
|
26
26
|
import { openPR } from './pr.mjs';
|
|
27
27
|
import { runVerification, exerciseUi } from './verify.mjs';
|
|
@@ -1080,54 +1080,49 @@ function runClaude({ prompt, cwd, tools, onEvent, onTodos, resume, model, effort
|
|
|
1080
1080
|
});
|
|
1081
1081
|
});
|
|
1082
1082
|
}
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
output_tokens: usage.output_tokens ?? usage.outputTokens ?? 0,
|
|
1088
|
-
cache_creation_input_tokens: usage.cache_creation_input_tokens ?? 0,
|
|
1089
|
-
cache_read_input_tokens: usage.cached_input_tokens ?? usage.cache_read_input_tokens ?? 0,
|
|
1090
|
-
};
|
|
1091
|
-
}
|
|
1092
|
-
function runCodex({ prompt, cwd, onEvent, permissionMode = 'acceptEdits', model, effort, onChild }) {
|
|
1083
|
+
// The Codex half of the worker bridge. Same contract as runClaude, because
|
|
1084
|
+
// nothing downstream may care which agent ran: live activity, the agent's own
|
|
1085
|
+
// to-dos (the board), the thread id to talk to it again, usage, final message.
|
|
1086
|
+
function runCodex({ prompt, cwd, onEvent, onTodos, resume, permissionMode = 'acceptEdits', model, effort, onChild }) {
|
|
1093
1087
|
return new Promise((done) => {
|
|
1094
1088
|
const sandbox = permissionMode === 'plan' ? 'read-only' : 'workspace-write';
|
|
1095
|
-
const args =
|
|
1089
|
+
const args = buildCodexArgs({ model: workerModel('codex', model), effort: workerEffort('codex', effort), sandbox, cwd, resume });
|
|
1096
1090
|
const child = spawn('codex', args, { cwd, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
1097
1091
|
onChild?.(child);
|
|
1098
1092
|
child.stdin.end(prompt);
|
|
1099
|
-
let result = '', err = '', buf = '', usage = null, timedOut = false, lastAct = '';
|
|
1093
|
+
let result = '', err = '', buf = '', sessionId = null, usage = null, timedOut = false, lastAct = '';
|
|
1100
1094
|
const timer = setTimeout(() => { timedOut = true; child.kill('SIGTERM'); }, WORKER_TIMEOUT_MS);
|
|
1101
1095
|
child.stdout.on('data', (d) => {
|
|
1102
1096
|
buf += d;
|
|
1103
1097
|
const lines = buf.split('\n'); buf = lines.pop();
|
|
1104
1098
|
for (const line of lines) {
|
|
1105
1099
|
if (!line.trim()) continue;
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1100
|
+
const evs = parseCodexEvents(line);
|
|
1101
|
+
// A line we cannot parse is still the worker talking — show it rather
|
|
1102
|
+
// than swallow it (how this ran before the bridge existed).
|
|
1103
|
+
if (!evs.length && !line.trim().startsWith('{')) {
|
|
1104
|
+
lastAct = line.trim().slice(0, 240);
|
|
1105
|
+
onEvent?.(lastAct);
|
|
1106
|
+
continue;
|
|
1107
|
+
}
|
|
1108
|
+
for (const ev of evs) {
|
|
1109
|
+
if (ev.kind === 'session') sessionId = ev.id;
|
|
1110
|
+
else if (ev.kind === 'usage') usage = ev.usage;
|
|
1111
|
+
else if (ev.kind === 'todos') onTodos?.(ev.todos);
|
|
1112
|
+
else if (ev.kind === 'message') {
|
|
1113
|
+
result = ev.text;
|
|
1114
|
+
onEvent?.(`✎ ${ev.text.trim().replace(/\s+/g, ' ').slice(0, 500)}`);
|
|
1115
|
+
} else if (ev.kind === 'error') {
|
|
1116
|
+
err += `${ev.text}\n`;
|
|
1117
|
+
onEvent?.(`Error ${ev.text}`);
|
|
1118
|
+
} else { lastAct = ev.text; onEvent?.(ev.text); }
|
|
1124
1119
|
}
|
|
1125
1120
|
}
|
|
1126
1121
|
});
|
|
1127
1122
|
child.stderr.on('data', (d) => { err += d; });
|
|
1128
1123
|
child.on('close', (code) => {
|
|
1129
1124
|
clearTimeout(timer);
|
|
1130
|
-
done({ code, timedOut, result: workerResultText({ result, err, code, timedOut, lastAct, timeoutMs: WORKER_TIMEOUT_MS }), sessionId
|
|
1125
|
+
done({ code, timedOut, result: workerResultText({ result, err, code, timedOut, lastAct, timeoutMs: WORKER_TIMEOUT_MS }), sessionId, usage });
|
|
1131
1126
|
});
|
|
1132
1127
|
});
|
|
1133
1128
|
}
|
package/package.json
CHANGED