@galda/cli 0.10.47 → 0.10.49

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 CHANGED
@@ -1329,13 +1329,17 @@
1329
1329
  color:var(--ink2);font:inherit;font-size:12.5px;white-space:nowrap;overflow:hidden;cursor:pointer;text-align:left;width:100%;position:relative}
1330
1330
  #fsRoot .fsproj:hover{background:var(--hover-s)}
1331
1331
  #fsRoot .fsproj.on{background:var(--hover);color:var(--ink)}
1332
- #fsRoot .fsproj i{width:4px;height:4px;border-radius:50%;background:var(--ink3);flex:0 0 4px}
1332
+ #fsRoot .fsproj i{width:4px;height:4px;border-radius:50%;background:var(--ink3);flex:0 0 4px;position:relative;top:1.5px} /* top:1.5px = optical nudge so the dot/running-ring centre sits on the name's x-height, not the line-box centre (Masa 2026-07-24, rail=D). Same shift for idle dot + running ring keeps the column aligned. */
1333
1333
  #fsRoot .fsproj.on i{background:var(--green)}
1334
1334
  /* running project = solid saturated green dot with a breathing glow — never dims to
1335
1335
  grey, independent of selection (HANDOFF-v45 §11, flagship .proj.run port) */
1336
- #fsRoot .fsproj.run i{background:var(--green);box-shadow:0 0 0 0 color-mix(in srgb,var(--green) 55%,transparent);animation:fsrundot 2s ease-in-out infinite}
1336
+ /* running project = a spinning broken green ring (matches the Doing marker · flagship #316).
1337
+ Was a pulsing dot (fsrundot). Centre WITHOUT transform (spin's rotate would override a centring
1338
+ translate → drift bottom-right) via top/left:50% + negative-half margin; concentric with the 4px dot. */
1339
+ #fsRoot .fsproj.run i{background:none;box-shadow:none;position:relative}
1340
+ #fsRoot .fsproj.run i::after{content:"";position:absolute;top:50%;left:50%;width:7px;height:7px;margin:-3.5px 0 0 -3.5px;box-sizing:border-box;border-radius:50%;border:1.3px solid #22c55e;border-top-color:transparent;animation:spin 1.8s linear infinite}
1337
1341
  @keyframes fsrundot{0%,100%{box-shadow:0 0 0 0 color-mix(in srgb,var(--green) 50%,transparent)}50%{box-shadow:0 0 5px 1.5px color-mix(in srgb,var(--green) 35%,transparent)}}
1338
- @media (prefers-reduced-motion:reduce){#fsRoot .fsproj.run i{animation:none}}
1342
+ @media (prefers-reduced-motion:reduce){#fsRoot .fsproj.run i::after{animation:none}}
1339
1343
  #fsRoot .fsproj .pname{min-width:0;overflow:hidden;text-overflow:ellipsis}
1340
1344
  #fsRoot .fsproj .num{margin-left:auto;font-size:10px;color:var(--ink3);flex:0 0 auto}
1341
1345
  /* per-project counts (HANDOFF-v45 §10): To Do = rounded-square icon, Review = Scan
@@ -2276,10 +2280,13 @@
2276
2280
  #fsRoot .trow .tt{color:color-mix(in srgb,var(--ink) 84%,var(--panel))} /* §13.2 soften To Do row name (not pure black); left chat feed keeps --ink */
2277
2281
  #fsRoot .trow:hover{background:var(--hover-s)}
2278
2282
  #fsRoot .trow.now{font-weight:500}
2279
- #fsRoot .trow .st{width:13px;height:13px;border-radius:50%;border:1.5px solid var(--hair2);flex:0 0 13px}
2280
- #fsRoot .trow .st.run{border-color:var(--green);background:radial-gradient(circle,rgba(var(--green-rgb),.4),transparent 70%)}
2281
- #fsRoot .trow .st.run.bad{border-color:var(--danger);background:radial-gradient(circle,rgba(var(--danger-rgb),.42),transparent 70%)}
2282
- #fsRoot .trow .st.run.int{border-color:var(--amber);background:radial-gradient(circle,rgba(var(--amber-rgb),.4),transparent 70%)}
2283
+ #fsRoot .trow .st{width:13px;height:13px;border-radius:50%;border:1.5px solid var(--hair2);flex:0 0 13px;position:relative;top:1px} /* top:1px = optical nudge so the whole status column (To Do/Doing/Failed/Done) centres on the row text, not the line-box (Masa 2026-07-24, board=C) */
2284
+ /* Doing marker = a broken green ring that spins (pattern B · flagship #316). Running only —
2285
+ .bad (failed) / .int (paused) stay static: no dead motion (DESIGN-RULES §5.5). Reuses @keyframes spin. */
2286
+ #fsRoot .trow .st.run{border-color:var(--green);border-top-color:transparent;background:none;box-shadow:0 0 6px color-mix(in srgb,var(--green) 40%,transparent);animation:spin 1.8s linear infinite}
2287
+ #fsRoot .trow .st.run.bad{border-color:var(--danger);background:radial-gradient(circle,rgba(var(--danger-rgb),.42),transparent 70%);animation:none;box-shadow:none}
2288
+ #fsRoot .trow .st.run.int{border-color:var(--amber);background:radial-gradient(circle,rgba(var(--amber-rgb),.4),transparent 70%);animation:none;box-shadow:none}
2289
+ @media (prefers-reduced-motion:reduce){#fsRoot .trow .st.run{animation:none}}
2283
2290
  /* Doing-failure state word (HANDOFF-v45 §11): colour TEXT only, no badge/pill surface —
2284
2291
  Failed = red, Interrupted = amber. */
2285
2292
  #fsRoot .trow .stword{font-size:10.5px;font-weight:600;color:var(--danger);flex:0 0 auto}
@@ -2380,7 +2387,7 @@
2380
2387
  /* GOTCHA (HANDOFF-v45 §11): the rule above wins over a plain .fsproj.run i by
2381
2388
  specificity and greys the running dot out on the Gradient wallpaper rail — this
2382
2389
  exception restores solid green so "running" never dims to grey. */
2383
- body[data-theme="gradient"] #fsRoot .fsproj.run i{background:#22c55e}
2390
+ body[data-theme="gradient"] #fsRoot .fsproj.run i{background:none}
2384
2391
  body[data-theme="gradient"] #fsRoot .fsproj .num,body[data-theme="gradient"] #fsRoot .fsproj .pcount{color:rgba(20,23,31,.6)}
2385
2392
  body[data-theme="gradient"] #fsRoot .fsproj:not(.on):hover{background:rgba(255,255,255,.34)}
2386
2393
  body[data-theme="gradient"] #fsRoot .fsproj.on{background:rgba(255,255,255,.5);color:var(--ink)}
@@ -2517,7 +2524,7 @@
2517
2524
  body[data-theme="gradient"][data-arcdark="1"] #fsRoot .fsproj{color:rgba(255,255,255,.85);text-shadow:0 1px 3px rgba(0,0,0,.35)}
2518
2525
  body[data-theme="gradient"][data-arcdark="1"] #fsRoot .fsproj i{background:rgba(255,255,255,.6)}
2519
2526
  /* same GOTCHA exception as the light Gradient rail, dark variant (HANDOFF-v45 §11) */
2520
- body[data-theme="gradient"][data-arcdark="1"] #fsRoot .fsproj.run i{background:#22c55e}
2527
+ body[data-theme="gradient"][data-arcdark="1"] #fsRoot .fsproj.run i{background:none}
2521
2528
  body[data-theme="gradient"][data-arcdark="1"] #fsRoot .fsproj .num,body[data-theme="gradient"][data-arcdark="1"] #fsRoot .fsproj .pcount{color:rgba(255,255,255,.6)}
2522
2529
  body[data-theme="gradient"][data-arcdark="1"] #fsRoot .fsproj.on{background:rgba(255,255,255,.15);color:#fff}
2523
2530
  body[data-theme="gradient"][data-arcdark="1"] #fsRoot .fsproj:not(.on):hover{background:rgba(255,255,255,.1)}
@@ -3323,7 +3330,7 @@ const savedTheme = (() => {
3323
3330
  if (v !== s) localStorage.setItem('theme', v);
3324
3331
  return v;
3325
3332
  })();
3326
- 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: 'default', editing: null, attach: [], connectCommand: '', models: ['sonnet'], agentModels: null, agentEfforts: null, agents: ['claude-code', 'codex'], expanded: new Set(), openThreads: new Set(), queueOrder: {}, externalActivity: [], selectedGoal: null, workflowColumns: {}, wfDraft: null, reviewDefinitions: {}, rdDraft: 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,
3333
+ 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'], expanded: new Set(), openThreads: new Set(), queueOrder: {}, externalActivity: [], selectedGoal: null, workflowColumns: {}, wfDraft: null, reviewDefinitions: {}, rdDraft: 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,
3327
3334
  // per-message send overrides set via slash-commands (reset after each send,
3328
3335
  // except model which is sticky in localStorage). palette = open command list.
3329
3336
  msg: { mode: null, model: null, effort: null, skill: null, later: false, review: false },
@@ -6142,6 +6149,13 @@ function render(){
6142
6149
  if (typeof SA !== 'undefined' && SA.open) saSync(); // keep the open See all Ledger live
6143
6150
  const bv = $('botver'); if (bv && state.uiVersion) bv.textContent = 'build ' + String(state.uiVersion).slice(0, 8);
6144
6151
  paintReplyNote(); // a reply's answer/question survives the re-render it caused
6152
+ // Persist the selected project so a reload (a real UI ship, or any reconnect)
6153
+ // restores the user's view instead of snapping back to 'default' (Masa
6154
+ // 2026-07-24 "勝手にデフォルトのプロジェクトに戻される"). Only store an id that
6155
+ // currently exists as a project, so onboarding/transient sentinels
6156
+ // ('__onboard') are never saved and the restore always lands on a real one;
6157
+ // an id that later disappears self-heals via the guards in refresh()/'projects'.
6158
+ if (state.projects.some((p) => p.id === state.active)) localStorage.setItem('sel:project', state.active);
6145
6159
  }
6146
6160
 
6147
6161
  // ============================================================================
@@ -6188,7 +6202,7 @@ function renderFsRail(){
6188
6202
  const activeProject = state.projects.find((p) => p.id === state.active);
6189
6203
  const activeLabel = $('fsProjBtnName');
6190
6204
  if (activeLabel) activeLabel.textContent = activeProject?.name ?? 'Projects';
6191
- el.innerHTML = state.projects.map((p) => {
6205
+ const html = state.projects.map((p) => {
6192
6206
  const ptasks = state.tasks.filter((t) => t.projectId === p.id);
6193
6207
  const pgoals = state.goals.filter((g) => g.projectId === p.id);
6194
6208
  // running dot (HANDOFF-v45 §11): a REAL running task, independent of selection —
@@ -6202,6 +6216,11 @@ function renderFsRail(){
6202
6216
  return `<button class="fsproj${p.id === state.active ? ' on' : ''}${running ? ' run' : ''}" data-fsproj="${p.id}"><i></i><span class="pname">${esc(p.name)}</span>${pcount}</button>`;
6203
6217
  }).join('')
6204
6218
  + `<button class="fsprojadd" id="fsProjAdd" title="New project"><span class="pa-plus"></span><span class="pa-label">New project</span></button>`;
6219
+ // Skip the DOM swap when the rail is unchanged: re-creating .fsproj.run would restart the
6220
+ // spinning ring's CSS animation — a visible "jump" on every SSE-driven re-render (Masa 2026-07-24).
6221
+ if (html === renderFsRail._html && el.childElementCount) return;
6222
+ renderFsRail._html = html;
6223
+ el.innerHTML = html;
6205
6224
  for (const b of el.querySelectorAll('[data-fsproj]')) b.onclick = (e) => {
6206
6225
  if (e.target.closest('.pname.editing') || b.querySelector('.pname.editing')) return;
6207
6226
  state.active = b.dataset.fsproj; document.body.classList.remove('projectsopen'); render();
package/engine/lib.mjs CHANGED
@@ -1086,7 +1086,15 @@ export function parseReviewSummary(raw, fallback = {}) {
1086
1086
  };
1087
1087
  }
1088
1088
 
1089
- const DOC_ONLY_RE = /(^|\/)(README|START-HERE|CHANGELOG|LICENSE)(\.[\w.-]+)?$|(^|\/)docs\/.*\.(md|mdx|txt)$|\.mdx?$|\.txt$/i;
1089
+ // A change is "doc-only" (skip the project test suite) when every file is
1090
+ // documentation. EVERYTHING under docs/ counts — not just .md: docs/ holds design
1091
+ // mockups (docs/design/**/*.html), handoffs, and images that the engine's tests
1092
+ // never import, so running the 58-file suite for them is pure waste. Before this,
1093
+ // only docs/**.md|mdx|txt was excused, so a CDO design-lab goal editing a single
1094
+ // docs/design/*.html re-ran the whole engine suite in its worktree and sat in
1095
+ // `running` ("Working on it") for 20+ minutes on a busy machine (dogfood 2026-07-24,
1096
+ // goal-632). Plus top-level README/CHANGELOG/LICENSE and any *.md/*.txt anywhere.
1097
+ const DOC_ONLY_RE = /(^|\/)(README|START-HERE|CHANGELOG|LICENSE)(\.[\w.-]+)?$|(^|\/)docs\/|\.mdx?$|\.txt$/i;
1090
1098
  export function hasTestRelevantChanges(changedFiles) {
1091
1099
  const files = (changedFiles ?? []).map((f) => String(f ?? '').trim()).filter(Boolean);
1092
1100
  if (!files.length) return true;
package/engine/server.mjs CHANGED
@@ -728,13 +728,22 @@ if (process.env.MANAGER_SEED_RUNNING_FIXTURE === '1') {
728
728
  // res -> identity (see identityFor), captured at connection time so every
729
729
  // SSE push can be scoped per-connection without re-deriving auth per event.
730
730
  const sseClients = new Map();
731
- // UI hot-reload: open tabs compare this stamp (index.html mtime + boot
732
- // time) via /api/state and the 30s SSE ping, and reload themselves when a
733
- // new UI ships — no manual reload after a hot-copy or a restart.
734
- const BOOT_STAMP = Date.now();
731
+ // UI hot-reload: open tabs compare this stamp (index.html mtime only) via
732
+ // /api/state and the 30s SSE ping, and reload themselves when a new UI ships
733
+ // — no manual reload after a hot-copy.
734
+ //
735
+ // It used to also fold in a per-process BOOT_STAMP (Date.now() at boot) so a
736
+ // bare restart would force a reload too. That backfired on the always-on
737
+ // resident: every relay-supervisor respawn / hot-copy restart, and every
738
+ // reconnect that landed on a *different* engine process (each with its own
739
+ // BOOT_STAMP), changed uiVersion and yanked a `location.reload()` on the 30s
740
+ // ping — the "reloads every 30-60s and resets to the default project" report
741
+ // (Masa 2026-07-24). mtime alone means a reload fires ONLY when app/index.html
742
+ // actually changes, and every process serving the same file agrees on the
743
+ // stamp, so restarts and multi-process relay churn no longer reload the tab.
735
744
  function uiVersion() {
736
- try { return `${Math.round(statSync(join(ROOT, 'app', 'index.html')).mtimeMs)}-${BOOT_STAMP}`; }
737
- catch { return String(BOOT_STAMP); }
745
+ try { return String(Math.round(statSync(join(ROOT, 'app', 'index.html')).mtimeMs)); }
746
+ catch { return '0'; }
738
747
  }
739
748
 
740
749
  // Broadcast to every connected client, unscoped — only for events that carry
@@ -4330,7 +4339,16 @@ const server = createServer(async (req, res) => {
4330
4339
  try {
4331
4340
  const { text } = JSON.parse(body);
4332
4341
  if (!text?.trim()) return json(res, 400, { error: 'text required' });
4333
- if (['partial', 'failed', 'interrupted', 'blocked'].includes(goal.status)) {
4342
+ // Reply picks a parked goal back up into the work queue. 'review' MUST be
4343
+ // here: a completed goal awaiting your approve/reject is 'review', and a
4344
+ // reply to it means "not done — keep working" (Masa 2026-07-24: replied to
4345
+ // an in-review card and it sat stuck in Review with no worker activity).
4346
+ // Without this flip the reply task was queued but the goal never left the
4347
+ // Review lane, so nothing looked like it was happening. Un-parking mirrors
4348
+ // the already-working 'blocked'/'failed'/etc. "Needs you" replies: the goal
4349
+ // flips to running and, since it still carries startedAt from its first run,
4350
+ // shows in Doing as "Working on it" — then the worker resumes its session.
4351
+ if (['review', 'partial', 'failed', 'interrupted', 'blocked'].includes(goal.status)) {
4334
4352
  goal.status = 'running';
4335
4353
  goal.blocked = null;
4336
4354
  saveGoal(goal);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@galda/cli",
3
- "version": "0.10.47",
3
+ "version": "0.10.49",
4
4
  "type": "module",
5
5
  "description": "Galda - hand off work to Claude Code or Codex, get proof back. Runs on your existing subscription, no extra API cost.",
6
6
  "scripts": {