@galda/cli 0.10.108 → 0.10.110
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +41 -0
- package/app/index.html +34 -30
- package/engine/lib.mjs +1 -1
- package/package.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -26,12 +26,53 @@
|
|
|
26
26
|
- 新しいロジックは可能な限り純関数として `engine/lib.mjs` 等に切り出し、`engine/test/` にテストを追加する。
|
|
27
27
|
- テストが書けない変更(純UI等)は、その理由と手動確認手順を報告に書く。
|
|
28
28
|
|
|
29
|
+
### テストの書き方(Masa体感「変に判断せずつなぐだけになった」を守る・2026-07-30 追加)
|
|
30
|
+
|
|
31
|
+
1日で main を2回赤にした型・3日間気付かれなかった型が実在する。以下は**好みでなく再発防止**。
|
|
32
|
+
|
|
33
|
+
- 🔴 **`app/index.html` のソース文字列を正規表現で照合するテストを新しく書かない。**
|
|
34
|
+
`align-items:center` や `outcome?.requirementEvidence?.…` という**式そのもの**を要求していたテストが、
|
|
35
|
+
実装の書き換えだけで赤くなった(2026-07-29 に PR #574/#584 の2回)。
|
|
36
|
+
**正規表現は「書き方を変えた」と「壊した」を区別できない。**
|
|
37
|
+
→ **実際に開いて、描かれたものを読む**。ヘルパは `engine/test/helpers/serve-app.mjs`+`helpers/browser.mjs`、
|
|
38
|
+
実例は `engine/test/review-delivery-label.test.mjs`/`review-requirement-coverage-ui.test.mjs`。
|
|
39
|
+
- 🔴 **テストを足したら、製品を壊して赤くなることを必ず確認する**(1〜3通り)。落ちないテストは何も証明しない。
|
|
40
|
+
確認したことと壊し方を PR 本文に書く。
|
|
41
|
+
- 🔴 **テストだけコミットして実装が無い状態を作らない。** 実例2件:
|
|
42
|
+
`5802056`(Reconnect のテストだけ main に入り実装が存在しなかった)/
|
|
43
|
+
**PR #396 が統合時に engine と app の実装を落とし、テストだけ入れた**(main が3日間、
|
|
44
|
+
存在しない機能のテストで赤かった。復元=PR #586)。
|
|
45
|
+
→ 複数レーンの成果を1本に統合する PR では、**テストだけでなく実装が入っているかを機械で照合する**
|
|
46
|
+
(`node tools/conflict-check.mjs` の `test-without-implementation`)。
|
|
47
|
+
- **transient な状態をポーリングで待たない。** 偽ワーカーは数msで返るので `running` は掴めない。
|
|
48
|
+
**残る証跡**(verdict・キューの中身・保存された状態)を待つ。時計を待つテストは飽和で揺れる。
|
|
49
|
+
- **赤を見つけたら、まず単体で再走**して「飽和フレーク」と「本物」を分ける。飽和で31秒かけて落ちるものが実在する
|
|
50
|
+
(`agent-picker-availability`)。素の `origin/main` と比べて自分の変更のせいかを判定する。
|
|
51
|
+
- **古い open PR をマージする前に、今日の `main` に載せて実機で確認する。**
|
|
52
|
+
main が既に別実装で解決済みなのに残っていた PR が実在(#425/#435/#509・全てクローズ)。
|
|
53
|
+
→ `node tools/conflict-check.mjs --pr <n>` の `already-in-main` 検査。
|
|
54
|
+
|
|
29
55
|
## デザイン(Masa指示 2026-07-06・必読)
|
|
30
56
|
|
|
31
57
|
- **UI(app/index.html・app/theme.css 等)に触る前に、必ず `docs/design/DESIGN-RULES.md` を読むこと**。特に §1.5 レイアウトの憲法(10条)と §5.5 美学。条文に反する実装はせず、変えたい場合は理由を添えて仲田さんの決定を仰ぐ。
|
|
32
58
|
- app/ のUIファイル編集時はhook(tools/design_rules_hook.sh)が自動でリマインドする。リマインドが出たら読んだ上で作業すること。
|
|
33
59
|
- 実装時の詳細契約は `docs/design/CTO-HANDOFF.md`(CDO→CTO引き継ぎ書)に従う。**既存パーツで作れない新しい画面・コンポーネントが必要になったら、自作せず実装前にCDOへ依頼すること。**
|
|
34
60
|
|
|
61
|
+
## プロダクトの不変条件(実装前に読む・2026-07-30 追加)
|
|
62
|
+
|
|
63
|
+
**正典 = `docs/PRD.md` §0(薄いインターフェース+4決定)と §0.1(信頼性の不変条件)。** 実装で迷ったらここに戻る。
|
|
64
|
+
Galda の役割は**依頼を Claude Code / Codex に繋ぐこと**で、途中に自前の AI 判断を挟まない。要約すると:
|
|
65
|
+
|
|
66
|
+
- **測っていない割合を主張しない**(キャッシュヒット率は節約ではない)。基準線が無い間は黙る。
|
|
67
|
+
- **判定は必ず最新の試行のもの**(文章が最新・絵や判定が最初、を作らない。同じ形のバグが3か所にあった)。
|
|
68
|
+
- **出荷していないものを成功として祝わない**(未マージでクローズされた PR を done にしない)。
|
|
69
|
+
- **人が頼んでいないものを要求として数えない**(Manager 発の返信を要求カバレッジに載せない)。
|
|
70
|
+
- **止まっているものは「なぜ」を言う**(`stacked` は動いても失敗してもいない=外からは壊れて見える)。
|
|
71
|
+
- **このマシンにできないことは先に言う**(欠けている実行ファイル名でなく、何ができなくなるかを言う)。
|
|
72
|
+
- **助言と停止判断を混ぜない**(`npm test` を開始できないものだけが blocking。混ぜると「走らせていないスイートが0 failed」になる)。
|
|
73
|
+
|
|
74
|
+
新しく「Galda が判断する」挙動を足したくなったら、**足す前に Masa の決定を仰ぐ**(§0 決定3の自動選択禁止と同じ理由)。
|
|
75
|
+
|
|
35
76
|
## 実装規律
|
|
36
77
|
|
|
37
78
|
- 変更は最小限。周辺コードの流儀に合わせる。
|
package/app/index.html
CHANGED
|
@@ -2279,19 +2279,6 @@
|
|
|
2279
2279
|
/* Changed/Tests aligns with the request TEXT, not the number/chevron — same 18px
|
|
2280
2280
|
the fold body below uses, so both sit under the same column (Masa 2026-07-29). */
|
|
2281
2281
|
.redesign .rqit .rqev{font-size:12px;color:var(--ink3);margin:5px 0 0;padding-left:18px}
|
|
2282
|
-
/* Canonical requirement coverage: one compact ledger, not three competing
|
|
2283
|
-
interpretations. Status is text + a small rule/mark (no badge surface),
|
|
2284
|
-
and Manager checks stay visually distinct from worker claims. */
|
|
2285
|
-
.redesign .rqcov{display:flex;flex-direction:column;margin-top:2px}
|
|
2286
|
-
.redesign .rqcov-row{padding:11px 0;border-top:1px solid var(--hair)}
|
|
2287
|
-
.redesign .rqcov-row:first-child{padding-top:0;border-top:0}
|
|
2288
|
-
.redesign .rqcov-main{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:14px;align-items:baseline}
|
|
2289
|
-
.redesign .rqcov-text{font-size:13.5px;line-height:1.55;color:var(--ink)}
|
|
2290
|
-
.redesign .rqcov-state{font:600 10px/1.4 var(--mono);letter-spacing:.02em;color:var(--ink3);white-space:nowrap}
|
|
2291
|
-
.redesign .rqcov-state.verified{color:var(--green)}
|
|
2292
|
-
.redesign .rqcov-state.failed{color:var(--danger)}
|
|
2293
|
-
.redesign .rqcov-detail{margin-top:5px;font-size:11.5px;line-height:1.55;color:var(--ink3)}
|
|
2294
|
-
.redesign .rqcov-detail b{font-weight:600;color:var(--ink2)}
|
|
2295
2282
|
/* A long ask folds with the SAME .lacc pulldown Activity/Conversation use — chevron on
|
|
2296
2283
|
the peek line, body is the genuine rest (no duplicate, no trailing "…": Masa
|
|
2297
2284
|
2026-07-29 flagged both the vertical anchor line and the "…" as unnecessary — the
|
|
@@ -3498,7 +3485,7 @@ const savedTheme = (() => {
|
|
|
3498
3485
|
if (v !== s) localStorage.setItem('theme', v);
|
|
3499
3486
|
return v;
|
|
3500
3487
|
})();
|
|
3501
|
-
const state = { repos: null, repoHome: '', /* GET /api/repos — the folders a project can point at (Context Bar) */ projects: [], goals: [], tasks: [], act: {}, actAt: {}, todos: {}, chatTurns: {}, active: localStorage.getItem('sel:project') || 'default', editing: null, attach: [], connectCommand: '', models: ['sonnet'], agentModels: null, agentEfforts: null, agents: ['claude-code', 'codex'], auth: null, expanded: new Set(), openThreads: new Set(), queueOrder: {}, externalActivity: [], selectedGoal: null, composerDetachedGoal: null, workflowColumns: {}, wfDraft: null, reviewDefinitions: {}, rdDraft: null, runningCounts: {}, parallelLimits: null, connectedApp: localStorage.getItem('sel:agent') || 'claude-code', reviewOpenGoal: null, reviewApprovals: {}, goalDetailOpen: null, qreplyOpen: new Set(), logCollapsed: localStorage.getItem('logCollapsed') !== '0', logEntries: [], layout: localStorage.getItem('layout') || 'flagship', theme: savedTheme, boardskin: localStorage.getItem('boardskin') || 'auto', summaryPattern: Number(localStorage.getItem('summaryPattern')) || 3, peraSpot: 0, skill: null, skillsCache: null, projectRules: {}, rulesOpen: false,
|
|
3488
|
+
const state = { repos: null, repoHome: '', /* GET /api/repos — the folders a project can point at (Context Bar) */ projects: [], goals: [], tasks: [], act: {}, actAt: {}, todos: {}, chatTurns: {}, active: localStorage.getItem('sel:project') || 'default', editing: null, attach: [], connectCommand: '', models: ['sonnet'], agentModels: null, agentEfforts: null, agents: ['claude-code', 'codex'], auth: null, expanded: new Set(), openThreads: new Set(), queueOrder: {}, externalActivity: [], selectedGoal: null, composerDetachedGoal: null, workflowColumns: {}, wfDraft: null, reviewDefinitions: {}, rdDraft: null, runningCounts: {}, parallelLimits: null, hostReadiness: [], connectedApp: localStorage.getItem('sel:agent') || 'claude-code', reviewOpenGoal: null, reviewApprovals: {}, goalDetailOpen: null, qreplyOpen: new Set(), logCollapsed: localStorage.getItem('logCollapsed') !== '0', logEntries: [], layout: localStorage.getItem('layout') || 'flagship', theme: savedTheme, boardskin: localStorage.getItem('boardskin') || 'auto', summaryPattern: Number(localStorage.getItem('summaryPattern')) || 3, peraSpot: 0, skill: null, skillsCache: null, projectRules: {}, rulesOpen: false,
|
|
3502
3489
|
// per-message send overrides set via slash-commands (reset after each send,
|
|
3503
3490
|
// except model which is sticky in localStorage). palette = open command list.
|
|
3504
3491
|
// Scoped per project (msgByProject) so a skill/mode picked in one project can't leak
|
|
@@ -8790,6 +8777,7 @@ async function refresh(){
|
|
|
8790
8777
|
}
|
|
8791
8778
|
state.connectCommand = s.connectCommand ?? ''; state.models = s.models ?? ['sonnet']; state.agentModels = s.agentModels ?? null; state.agentEfforts = s.agentEfforts ?? null; state.agents = s.agents ?? ['claude-code', 'codex']; state.uiVersion = s.uiVersion;
|
|
8792
8779
|
state.billing = s.billing ?? { licensed: false, email: null };
|
|
8780
|
+
state.hostReadiness = s.hostReadiness ?? [];
|
|
8793
8781
|
renderAppSelect(); // picker follows the server's installed-agent set; may correct a stale connectedApp
|
|
8794
8782
|
renderAuthBanner(s.auth); // only the selected worker's auth can raise a banner
|
|
8795
8783
|
setAgentModel(state.connectedApp);
|
|
@@ -9553,7 +9541,7 @@ function closeSummaryPera(){ $('summaryOverlay').classList.remove('show'); }
|
|
|
9553
9541
|
// 4 archived / 5 reverted — session-local; the server round-trip is what
|
|
9554
9542
|
// settles it (see saSync's "returned to review" reset).
|
|
9555
9543
|
// ============================================================================
|
|
9556
|
-
const SA = { open: false, items: [], diffs: {}, actLog: {}, msgs: {}, convOpen: new Set(), shotBox: {}, detOpen: new Set(), diffOpen: new Set(), attachments: {}, focusGid: null, focusScrolled: false, focusTimer: null, cur: null, solo: null, lastMarkup: null };
|
|
9544
|
+
const SA = { open: false, items: [], diffs: {}, actLog: {}, msgs: {}, msgCounts: {}, convOpen: new Set(), shotBox: {}, detOpen: new Set(), diffOpen: new Set(), attachments: {}, focusGid: null, focusScrolled: false, focusTimer: null, cur: null, solo: null, lastMarkup: null };
|
|
9557
9545
|
// Same convention as the existing ?dev=1 devbar flag — off unless someone opts in by URL.
|
|
9558
9546
|
const SA_DEV = new URLSearchParams(location.search).get('dev') === '1';
|
|
9559
9547
|
// task-971: 白黒(モノクロ)版のレールを ?railmono=1 で見比べられるようにする。
|
|
@@ -10118,7 +10106,13 @@ function saReqStackRows(it, attemptEvidence){
|
|
|
10118
10106
|
const gk = it.goalId;
|
|
10119
10107
|
const reqMsgs = SA.msgs[gk];
|
|
10120
10108
|
const reqAsks = Array.isArray(reqMsgs) ? reqMsgs.filter((m) => m?.from === 'you' && String(m.text || '').trim()) : [];
|
|
10121
|
-
|
|
10109
|
+
// The frozen attempt ledger is task-backed and available on first paint. Use it
|
|
10110
|
+
// before the asynchronously loaded conversation so Request #2/#3 cannot vanish
|
|
10111
|
+
// because the browser still holds an older messages cache.
|
|
10112
|
+
const ledgerAsks = Array.isArray(it.attempt?.requirements)
|
|
10113
|
+
? it.attempt.requirements.filter((r) => ['initial', 'reply'].includes(r?.kind) && String(r.text || '').trim())
|
|
10114
|
+
: [];
|
|
10115
|
+
const reqItems = ledgerAsks.length ? ledgerAsks : reqAsks.length ? reqAsks : (it.goalText ? [{ text: it.goalText }] : []);
|
|
10122
10116
|
const row = (m, idx) => {
|
|
10123
10117
|
const text = String(m.text || '').trim();
|
|
10124
10118
|
const isLatest = idx === reqItems.length - 1;
|
|
@@ -10275,18 +10269,6 @@ function saItemHtml(it, i){
|
|
|
10275
10269
|
const summarySec = summaryText
|
|
10276
10270
|
? `<section class="rsec rsum"><div class="rsec-h">Summary</div><p class="rbody v">${esc(summaryText.slice(0, 200))}</p></section>`
|
|
10277
10271
|
: '';
|
|
10278
|
-
const coverage = (it.requirementCoverage || []).filter((r) => r.text);
|
|
10279
|
-
const coverageRows = coverage.map((r) => {
|
|
10280
|
-
const labels = { verified: 'Verified', failed: 'Failed', 'not-applicable': 'Not applicable', unverified: 'Not verified' };
|
|
10281
|
-
const details = [
|
|
10282
|
-
r.legacy ? 'Legacy item — no canonical evidence was recorded' : '',
|
|
10283
|
-
r.checkFacts?.length ? `Manager: ${r.checkFacts.join(' · ')}` : '',
|
|
10284
|
-
r.claims?.length ? `Worker claim: ${r.claims.join(', ')}` : '',
|
|
10285
|
-
r.workerEvidence?.length ? r.workerEvidence.join(' · ') : '',
|
|
10286
|
-
].filter(Boolean);
|
|
10287
|
-
return `<div class="rqcov-row" data-requirement-id="${esc(r.id)}"><div class="rqcov-main"><div class="rqcov-text">${esc(r.text)}</div><span class="rqcov-state ${esc(r.coverage)}">${esc(labels[r.coverage] || labels.unverified)}</span></div>${details.length ? `<div class="rqcov-detail">${details.map((detail, index) => index === 0 ? esc(detail) : `<br>${esc(detail)}`).join('')}</div>` : ''}</div>`;
|
|
10288
|
-
}).join('');
|
|
10289
|
-
const coverageSec = coverageRows ? `<section class="rsec"><div class="rsec-h">Requirement coverage</div><div class="rqcov">${coverageRows}</div></section>` : '';
|
|
10290
10272
|
// ── Open it (Masa 2026-07-22): a plain section in the card's own typography — the same
|
|
10291
10273
|
// heading + body shape as Request and Result, not a chip in another vocabulary. The
|
|
10292
10274
|
// address the worker declared IS the control (press it → the process starts on demand →
|
|
@@ -10534,7 +10516,6 @@ function saItemHtml(it, i){
|
|
|
10534
10516
|
${reqSec}
|
|
10535
10517
|
${S('Open it', openBody + infraBody)}
|
|
10536
10518
|
${S('What to check', checkBody)}
|
|
10537
|
-
${coverageSec}
|
|
10538
10519
|
${S('Run state', interruptedBody)}
|
|
10539
10520
|
${S('PR', prRepairBody)}
|
|
10540
10521
|
${S('Also carries', carriesBody)}
|
|
@@ -10583,8 +10564,11 @@ async function saLoadActivityLog(taskId){
|
|
|
10583
10564
|
// request writes an honest line instead of an empty box — the same rule the
|
|
10584
10565
|
// implementation log follows.
|
|
10585
10566
|
async function saLoadMessages(goalId){
|
|
10586
|
-
if (!Number.isFinite(goalId)
|
|
10567
|
+
if (!Number.isFinite(goalId)) return;
|
|
10568
|
+
const expectedCount = Number(SA.items.find((x) => x.goalId === goalId)?.messageCount) || 0;
|
|
10569
|
+
if (SA.msgs[goalId] !== undefined && SA.msgCounts[goalId] === expectedCount) return;
|
|
10587
10570
|
SA.msgs[goalId] = null; // in flight — a second toggle must not re-fetch
|
|
10571
|
+
SA.msgCounts[goalId] = expectedCount;
|
|
10588
10572
|
let msgs = [];
|
|
10589
10573
|
try {
|
|
10590
10574
|
const r = await fetch(withKey(`/api/goals/${goalId}/messages`));
|
|
@@ -12540,6 +12524,7 @@ function gearSetHTML(){
|
|
|
12540
12524
|
${billingRow}
|
|
12541
12525
|
${stability}
|
|
12542
12526
|
${gearRulesHTML()}
|
|
12527
|
+
${hostReadinessRow()}
|
|
12543
12528
|
<div class="srow dis"><div class="sl">Data folder<span class="sd">Runs are stored in the engine's chat-runs folder on the host</span></div></div>`;
|
|
12544
12529
|
}
|
|
12545
12530
|
async function loadStability(){
|
|
@@ -12552,6 +12537,25 @@ async function loadStability(){
|
|
|
12552
12537
|
// does NOT own these rules, it only surfaces them (editing is done in an editor or
|
|
12553
12538
|
// by asking the AI in chat). §3 段階開示 — when the project has no such file the row
|
|
12554
12539
|
// is absent entirely (never an empty container; the "ask once" flow is §1.5b).
|
|
12540
|
+
// Two capabilities degrade silently depending on the machine: without Chrome
|
|
12541
|
+
// nothing can screenshot or verify in a browser, and without a native picker
|
|
12542
|
+
// "Open folder…" cannot open Finder. The server states them in /api/state
|
|
12543
|
+
// (hostReadiness); this is where a person can read them before spending an hour
|
|
12544
|
+
// treating the absence as a bug.
|
|
12545
|
+
//
|
|
12546
|
+
// Same row shape as Data folder — a host fact, no chip, nothing to press (§SETTINGS
|
|
12547
|
+
// 行=ラベル+説明). When everything is available it stays dimmed like its neighbour;
|
|
12548
|
+
// when something is missing the row is NOT dimmed, because a dimmed row at .38 is
|
|
12549
|
+
// exactly the thing nobody reads. No colour and no fill either way (§1.5-8: an
|
|
12550
|
+
// exception is a small inline element, never a painted surface) — this is a
|
|
12551
|
+
// capability note, not an error.
|
|
12552
|
+
function hostReadinessRow(){
|
|
12553
|
+
const checks = state.hostReadiness || [];
|
|
12554
|
+
if (!checks.length) return ''; // nothing measured → no row (§1.5-3)
|
|
12555
|
+
const missing = checks.filter((c) => !c.ok);
|
|
12556
|
+
const detail = (missing.length ? missing : checks).map((c) => c.detail).join(' · ');
|
|
12557
|
+
return `<div class="srow${missing.length ? '' : ' dis'}"><div class="sl">This machine<span class="sd">${esc(detail)}</span></div></div>`;
|
|
12558
|
+
}
|
|
12555
12559
|
function gearRulesHTML(){
|
|
12556
12560
|
const r = state.projectRules[state.active];
|
|
12557
12561
|
if (!r || !r.files || !r.files.length) return '';
|
package/engine/lib.mjs
CHANGED
|
@@ -1174,7 +1174,7 @@ export function buildReviewAttemptLedger({ goal = null, tasks = [] } = {}) {
|
|
|
1174
1174
|
const outcome = [...settled].sort((a, b) => time(b) - time(a))[0] ?? null;
|
|
1175
1175
|
const requirements = [
|
|
1176
1176
|
goal?.text ? { id: `goal-${goal.id}-initial`, kind: 'initial', text: String(goal.text), at: goal.createdAt ?? null } : null,
|
|
1177
|
-
...own.filter((task) => task?.reply && task?.detail).map((task) => ({
|
|
1177
|
+
...own.filter((task) => task?.reply && task?.detail && (!task.replyFrom || task.replyFrom === 'you')).map((task) => ({
|
|
1178
1178
|
id: task.attemptId || `task-${task.id}`,
|
|
1179
1179
|
kind: 'reply', text: String(task.detail), at: task.createdAt ?? null,
|
|
1180
1180
|
})),
|
package/package.json
CHANGED