@galda/cli 0.10.78 → 0.10.80

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
@@ -3595,6 +3595,12 @@ function reviewCardConfigFor(projectId){ return { ...DEFAULT_REVIEW_DEFINITION.r
3595
3595
  // goal's own request language, same as the planner/worker; ja/en pin the
3596
3596
  // project to one language regardless of what a given goal was typed in.
3597
3597
  function reviewLangFor(projectId){ const l = reviewDefinitionFor(projectId).language; return (l === 'en' || l === 'ja') ? l : 'auto'; }
3598
+ function retryCopy(projectId){
3599
+ const ja = reviewLangFor(projectId) === 'ja';
3600
+ return ja
3601
+ ? { infra: '検証基盤エラー', retry: '再検証', retrying: '再検証中…', failed: '再検証に失敗' }
3602
+ : { infra: 'Verification infrastructure error', retry: 'Retry verification', retrying: 'Retrying…', failed: 'Verification retry failed' };
3603
+ }
3598
3604
 
3599
3605
  function esc(s){ const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; }
3600
3606
 
@@ -4636,7 +4642,7 @@ function renderTasks(){
4636
4642
  const rs = g.reviewSummary ?? {};
4637
4643
  const checkLine = (rs.check || g.plan?.[0] || g.text || '').replace(/\s+/g, ' ').slice(0, 58);
4638
4644
  return `<div class="task revrow" data-goal="${g.id}"><div class="ic fail">!</div><div class="tx"><div class="name">${esc(checkLine)} ${goalSourceBadge(g.source)}</div>
4639
- <div class="cond"><span class="blockreason">${esc(g.blocked?.reason ?? 'blocked')}</span> <button type="button" class="retrybtn" data-goalreverify="${g.id}">再検証</button> <button type="button" class="retrybtn skip" data-archivegoal="${g.id}" title="このゴールを片付ける(再作業なし)">Archive</button></div></div></div>`;
4645
+ <div class="cond"><span class="blockreason">${esc(g.blocked?.reason ?? 'blocked')}</span> <button type="button" class="retrybtn" data-goalreverify="${g.id}">${retryCopy(g.projectId).retry}</button> <button type="button" class="retrybtn skip" data-archivegoal="${g.id}" title="Archive without rework">Archive</button></div></div></div>`;
4640
4646
  }).join('');
4641
4647
  const attention = blockedGoals + list.filter((t) => ['failed', 'interrupted'].includes(t.status)).map((t) => row(t, { retry: 1 })).join('');
4642
4648
  const doing = grouped(list.filter((t) => t.status === 'running'), (t) => row(t));
@@ -4833,10 +4839,10 @@ function renderTasks(){
4833
4839
  }
4834
4840
  // Manager-verified gate: re-run tests + re-capture proof for a blocked goal.
4835
4841
  for (const b of $('tasklist').querySelectorAll('[data-goalreverify]')) {
4836
- b.onclick = async (e) => { e.stopPropagation(); b.disabled = true; b.textContent = '再検証中…';
4842
+ b.onclick = async (e) => { e.stopPropagation(); const copy = retryCopy(state.active); b.disabled = true; b.textContent = copy.retrying;
4837
4843
  let r; try { r = await fetch(withKey(`/api/goals/${b.dataset.goalreverify}/reverify`), { method: 'POST' }); }
4838
- catch { showErr('再検証に失敗'); return; }
4839
- qrefresh(r, '再検証に失敗'); };
4844
+ catch { showErr(copy.failed); return; }
4845
+ qrefresh(r, copy.failed); };
4840
4846
  }
4841
4847
  for (const b of $('tasklist').querySelectorAll('[data-qreply]')) {
4842
4848
  b.onclick = (e) => { e.stopPropagation();
@@ -5404,7 +5410,7 @@ function renderReviewChecklist(){
5404
5410
  : '';
5405
5411
  const infra = goal.verificationInfraError;
5406
5412
  const infraHtml = infra?.detail
5407
- ? `<div class="rvinfra"><span class="rvlbl">${esc(infra.title || '検証基盤エラー')}</span>${esc(infra.detail)} <button type="button" class="retrybtn" data-goalreverify="${goal.id}">再検証</button></div>`
5413
+ ? `<div class="rvinfra"><span class="rvlbl">${esc(infra.title || retryCopy(goal.projectId).infra)}</span>${esc(infra.detail)} <button type="button" class="retrybtn" data-goalreverify="${goal.id}">${retryCopy(goal.projectId).retry}</button></div>`
5408
5414
  : '';
5409
5415
  $('reviewSummary').innerHTML =
5410
5416
  vfyHtml
@@ -5477,10 +5483,10 @@ function renderReviewChecklist(){
5477
5483
  b.onclick = async (e) => {
5478
5484
  e.stopPropagation();
5479
5485
  b.disabled = true;
5480
- b.textContent = '再検証中…';
5486
+ b.textContent = retryCopy(goal.projectId).retrying;
5481
5487
  let r; try { r = await fetch(withKey(`/api/goals/${b.dataset.goalreverify}/reverify`), { method: 'POST' }); }
5482
- catch { showErr('再検証に失敗'); b.disabled = false; return; }
5483
- if (r.ok) await refresh(); else showErr('再検証に失敗');
5488
+ catch { showErr(retryCopy(goal.projectId).failed); b.disabled = false; return; }
5489
+ if (r.ok) await refresh(); else showErr(retryCopy(goal.projectId).failed);
5484
5490
  };
5485
5491
  }
5486
5492
  for (const b of $('reviewList').querySelectorAll('.reqsend')) {
@@ -6822,7 +6828,10 @@ function renderFsBoard(){
6822
6828
  // errored is status-based now (blocked/failed/partial/interrupted live in the Needs-you column,
6823
6829
  // not Doing). Doing holds only what's actively moving, so bkt==='doing' ⇒ healthy in flight.
6824
6830
  const errored = ['blocked', 'failed', 'partial', 'interrupted'].includes(g.status);
6825
- const intr = g.status === 'interrupted';
6831
+ // `partial` can mean an earlier step completed and the latest one was
6832
+ // interrupted. Read the concrete task so this card doesn't turn amber,
6833
+ // resumable work into a red generic failure.
6834
+ const intr = g.status === 'interrupted' || list.some((t) => t.goalId === g.id && t.status === 'interrupted');
6826
6835
  const isPaused = g.status === 'blocked' && g.blocked?.kind === 'paused'; // 一時停止=エラーでなくアンバー扱い
6827
6836
  const amber = intr || isPaused;
6828
6837
  const healthyRun = bkt === 'doing' && !errored; // paused(=errored) goals now sit in 'doing' too but carry no progress bar
@@ -7155,7 +7164,10 @@ function errorButtonsHtml(meta, { taskId = null, goalId = null, interrupted = fa
7155
7164
  const out = [];
7156
7165
  for (const id of meta.actionIds || []) {
7157
7166
  if (id === 'retry' && taskId != null) out.push(`<button class="errbtn" data-fsretry="${taskId}">${interrupted ? C.resume : C.retry}</button>`);
7158
- else if (id === 'run-with-codex' && canCodex) out.push(`<button class="errbtn" data-fscodex="${goalId}">${C.other}</button>`);
7167
+ // This keeps the existing agent-switch endpoint, but says what it really does
7168
+ // for an interrupted run: continue the saved goal with another agent rather
7169
+ // than imply a brand-new, contextless task will be created.
7170
+ else if (id === 'run-with-codex' && canCodex) out.push(`<button class="errbtn" data-fscodex="${goalId}">${interrupted ? (failureLang() === 'ja' ? 'Codexで再開' : 'Resume with Codex') : C.other}</button>`);
7159
7171
  else if (id === 'connect') out.push(`<button class="errbtn" data-fsconnectworker="1">${C.connect}</button>`);
7160
7172
  else if (id === 'check-connection') out.push(`<button class="errbtn" data-fscheckworker="1">${C.check}</button>`);
7161
7173
  else if (id === 'retry-tests' && goalId != null) out.push(`<button class="errbtn" data-fsreverify="${goalId}">${C.tests}</button>`);
@@ -7291,10 +7303,6 @@ function renderFsTodo(){
7291
7303
  const needsCount = failed.filter((t) => !resumedGoalIds.has(t.goalId)).length + blocked.length + needsInput.length;
7292
7304
  const doingCount = running.length + finalizingGoals.length + pausedGoalIds.size;
7293
7305
  const nextCount = parkedReplyGoals.length + queued.length;
7294
- const hasPipelineWork = doingCount > 0 || nextCount > 0 || pgoals.some((g) => ['planning', 'stacked'].includes(g.status));
7295
- const laterIdle = later.length && !hasPipelineWork
7296
- ? `<div class="lateridle"><span class="msg">${document.documentElement.lang === 'ja' ? `実行待ちはありません。Laterの${later.length}件は自動開始されません。` : `Nothing is queued. ${later.length} Later item${later.length === 1 ? '' : 's'} will not start automatically.`}</span><button data-fsstartnextlater="${later[0].id}">${document.documentElement.lang === 'ja' ? '次を開始' : 'Start next'}</button></div>`
7297
- : '';
7298
7306
  // Free-tier pending counter (H22): on the free plan the header shows the wall
7299
7307
  // — pending goals out of the 5-slot cap — reddening at the cap and gone once
7300
7308
  // paid. Reads the SAME numbers the gate enforces (state.billing, never a local
@@ -7316,7 +7324,6 @@ function renderFsTodo(){
7316
7324
  <div class="qadd" data-fsnew="1" role="button" tabindex="0" title="New goal"><span class="paplus"></span><span>New</span></div>
7317
7325
  <span class="tlcap">Later</span>
7318
7326
  <div class="qadd lateradd" data-fslaternew="1" role="button" tabindex="0" title="New Later goal"><span class="paplus"></span><span>New</span></div>
7319
- ${laterIdle}
7320
7327
  ${laterRows}
7321
7328
  </div>
7322
7329
  ${doneAll.length ? `<div class="doneline${fsUI.doneOpen ? ' open' : ''}" id="fsDoneLine">✓ Done<span class="num">${doneAll.length}</span><span class="fold">▾</span></div>
@@ -7381,13 +7388,6 @@ function renderFsTodo(){
7381
7388
  let r; try { r = await fetch(withKey(`/api/goals/${b.dataset.fspstart}/activate`), { method: 'POST' }); }
7382
7389
  catch { showErr('Start failed.'); return; }
7383
7390
  if (!r.ok) showErr('Start failed.'); else await refresh(); };
7384
- const startNextLater = el.querySelector('[data-fsstartnextlater]');
7385
- if (startNextLater) startNextLater.onclick = async (e) => { e.stopPropagation();
7386
- startNextLater.disabled = true;
7387
- let r; try { r = await fetch(withKey(`/api/goals/${startNextLater.dataset.fsstartnextlater}/activate`), { method: 'POST' }); }
7388
- catch { showErr(document.documentElement.lang === 'ja' ? '開始できませんでした' : 'Could not start the next item.'); startNextLater.disabled = false; return; }
7389
- if (!r.ok) { showErr(document.documentElement.lang === 'ja' ? '開始できませんでした' : 'Could not start the next item.'); startNextLater.disabled = false; }
7390
- else await refresh(); };
7391
7391
  for (const b of el.querySelectorAll('[data-fspdel]')) b.onclick = async (e) => { e.stopPropagation();
7392
7392
  const preview = b.closest('.trow')?.querySelector('.tt')?.textContent || '';
7393
7393
  if (!(await confirmDelete({ title: 'Delete this shelved goal?', preview }))) return;
@@ -8927,7 +8927,14 @@ async function send(){
8927
8927
  // review goal → the AI decides continue/rescope/split/answer (same path as the
8928
8928
  // review card's own reply box), with the note anchored under the composer.
8929
8929
  const body = await replyToGoal(bind.goal.id, fullText, '#input');
8930
- if (body) { upsert(state.goals, body.goal); if (body.task) upsert(state.tasks, body.task); render(); }
8930
+ if (body) {
8931
+ upsert(state.goals, body.goal); if (body.task) upsert(state.tasks, body.task);
8932
+ // A Review reply is a one-shot handoff. Keeping the composer bound
8933
+ // afterwards adds a redundant “Replying to #N” banner and makes the
8934
+ // next, unrelated instruction easy to send to the old goal by mistake.
8935
+ state.selectedGoal = null;
8936
+ render();
8937
+ }
8931
8938
  } else {
8932
8939
  // running/partial/failed/interrupted/blocked → a plain follow-up on the same goal.
8933
8940
  const r = await fetch(withKey(`/api/goals/${bind.goal.id}/reply`), { method: 'POST',
@@ -9172,6 +9179,11 @@ function latestGoalRunForOutcome(goalId, tasks, outcome){
9172
9179
  function saRowFromDigest(r, gs, ts){
9173
9180
  const g = gs.find((x) => x.id === r.goalIds[0]);
9174
9181
  const outcome = latestGoalOutcomeTask(g?.id, ts);
9182
+ // A timeout commonly settles its parent goal as `partial`: some earlier work
9183
+ // may be real, while the final task is retryable. Preserve the task-level truth
9184
+ // here so the card never calls that state a generic failure.
9185
+ const interruptedTask = [...ts].filter((t) => r.goalIds.includes(t.goalId) && t.status === 'interrupted')
9186
+ .sort((a, b) => taskSortTime(b) - taskSortTime(a))[0] || null;
9175
9187
  const pt = ts.find((t) => r.goalIds.includes(t.goalId) && t.proof && (t.proof.png || t.proof.gif));
9176
9188
  // "What we did" (H23 §2, replaces the old Plan/Review-thread folds): one bullet
9177
9189
  // per finished requirement task's own report; falls back to the goal's plan
@@ -9201,6 +9213,7 @@ function saRowFromDigest(r, gs, ts){
9201
9213
  .map((x) => ({ id: x.id, title: saReviewTitle(x) }));
9202
9214
  return {
9203
9215
  ids: r.goalIds, goalId: r.goalIds[0], goal: g, folded,
9216
+ interruptedTask,
9204
9217
  no: r.reviewNumber,
9205
9218
  title: saReviewTitle(g),
9206
9219
  check: String(g?.reviewSummary?.check || r.checkLine || '').replace(/\s+/g, ' ').trim(),
@@ -9727,10 +9740,27 @@ function saItemHtml(it, i){
9727
9740
  ? `<div class="ropen rdeclerr"><p class="rbody v">${failureLang() === 'ja' ? 'AIが開けるものを用意しようとしましたが、記録が読み取れませんでした。返信して直させてください。' : 'The AI tried to declare something to open here, but the record was unreadable. Reply to have it rewritten.'}</p></div>`
9728
9741
  : '';
9729
9742
  const openBody = (it.run?.cmd || it.run?.url)
9730
- ? `<div class="ropen" id="runlocal${i}"><button type="button" class="olink" data-saact="preview:${i}" id="prevlink${i}">${esc(it.run.url || 'Start it')}<span class="ar">↗</span></button><p class="rbody v">${esc(it.run.note || it.run.cmd || 'Starts when you press it.')}</p></div>`
9743
+ ? `<div class="ropen" id="runlocal${i}"><button type="button" class="olink" data-saact="preview:${i}" id="prevlink${i}">${esc(it.run.url || 'Start it')}<span class="ar">↗</span></button><p class="rbody v">${esc(it.run.note || it.run.cmd || 'Starts when you press it.')}${it.interruptedTask ? ' Available now; it does not wait for this run to finish.' : ''}</p></div>`
9731
9744
  : it.pr
9732
9745
  ? `<div class="ropen"><a class="olink" href="${esc(it.pr)}" target="_blank" rel="noopener">${esc(it.pr)}<span class="ar">↗</span></a><p class="rbody v">Review and merge this pull request to finish the item.</p></div>`
9733
9746
  : runDeclErrorBody;
9747
+ // An interrupted worker is not a failed implementation. Keep the recovery
9748
+ // record factual and compact: the last recorded step, files already changed,
9749
+ // whether verification remains, and the exact resume action. These are all
9750
+ // state fields — no Manager-generated diagnosis or task re-planning.
9751
+ const interruptedBody = it.interruptedTask ? (() => {
9752
+ const t = it.interruptedTask;
9753
+ const last = (state.act[t.id] ?? t.activity ?? []).filter(Boolean).at(-1);
9754
+ const files = [...new Set((t.changedFiles ?? []).concat(filePaths))];
9755
+ const tests = it.testResult?.ran
9756
+ ? (it.testResult.ok ? 'Verification completed before the interruption.' : 'Verification remains incomplete or failed.')
9757
+ : 'Verification has not completed yet.';
9758
+ const reason = String(t.result || 'The worker reached its run limit before finishing.').replace(/\s+/g, ' ').slice(0, 240);
9759
+ return `<div class="rinfra"><p class="rbody v"><b>Interrupted — resume available</b><br>${esc(reason)}</p>`
9760
+ + (last ? `<p class="rbody v">Last recorded step: ${esc(String(last).slice(0, 220))}</p>` : '')
9761
+ + (files.length ? `<p class="rbody v">Changed so far: ${esc(files.join(' · '))}</p>` : '')
9762
+ + `<p class="rbody v">${esc(tests)}</p><div class="acts2"><button class="errbtn" data-saact="resume:${i}">Resume</button>${errorButtonsHtml(taskErrorMeta(t, it.goal), { taskId: null, goalId: it.goalId, interrupted: true, agent: t.agent || it.goal?.agent })}</div></div>`;
9763
+ })() : '';
9734
9764
  const prRepairBody = it.goal?.prError
9735
9765
  ? `<div class="prfail"><b>${esc(it.goal.prRepair?.title || 'PR blocked')}</b> <span class="dim">${esc(it.goal.prError)}</span>${it.goal.prRepair?.steps?.length ? `<ul>${it.goal.prRepair.steps.map((s) => `<li>${esc(s)}</li>`).join('')}</ul>` : ''}${it.goal.prRepair?.dirtyFiles?.length ? `<div class="dim">dirty: ${it.goal.prRepair.dirtyFiles.map(esc).join(' · ')}</div>` : ''}<div class="acts2"><button class="errbtn" data-saact="retrypr:${i}">${failureLang() === 'ja' ? 'PR再試行' : 'Retry PR'}</button><button class="errbtn" data-saact="glog:${i}">${failureLang() === 'ja' ? 'ログ確認' : 'View log'}</button></div></div>`
9736
9766
  : '';
@@ -9893,8 +9923,8 @@ function saItemHtml(it, i){
9893
9923
  const attn = !working && ['blocked', 'partial', 'failed', 'interrupted'].includes(gstatus);
9894
9924
  // Match the flagship list's state colours (Masa's Interrupt=amber / Failed=red rule): an
9895
9925
  // interrupted goal is amber+resumable, everything else stopped is a red error.
9896
- const attnInt = gstatus === 'interrupted';
9897
- const attnLabel = gstatus === 'interrupted' ? 'Interrupted' : gstatus === 'blocked' ? 'Blocked' : 'Failed';
9926
+ const attnInt = gstatus === 'interrupted' || Boolean(it.interruptedTask);
9927
+ const attnLabel = attnInt ? 'Interrupted resume available' : gstatus === 'blocked' ? 'Blocked' : 'Failed';
9898
9928
  const rst = working
9899
9929
  ? '<span class="rst working"><span class="rdot"></span>Working on it</span>'
9900
9930
  : attn
@@ -9906,6 +9936,7 @@ function saItemHtml(it, i){
9906
9936
  ${meta}
9907
9937
  ${reqSec}
9908
9938
  ${S('Open it', openBody)}
9939
+ ${S('Run state', interruptedBody)}
9909
9940
  ${S('PR', prRepairBody)}
9910
9941
  ${S('Also carries', carriesBody)}
9911
9942
  ${S('What to check', checkBody)}
@@ -10390,6 +10421,11 @@ $('seeall').addEventListener('click', async (e) => {
10390
10421
  }
10391
10422
  const it = SA.items[i];
10392
10423
  if (!it) return;
10424
+ if (k === 'resume') {
10425
+ const taskId = it.interruptedTask?.id;
10426
+ if (taskId != null) return fsRetryTask(taskId);
10427
+ return;
10428
+ }
10393
10429
  if (k === 'retrypr') return fsRetryPrGoal({ disabled: false, textContent: '', dataset: { fsprretry: String(it.goalId) } });
10394
10430
  if (k === 'glog') return toggleGoalDetail(it.goalId);
10395
10431
  // meta-row toggle (H23): tap the token chip to reveal the technique breakdown.
package/engine/server.mjs CHANGED
@@ -2593,7 +2593,11 @@ async function startPreview(task) {
2593
2593
  // They never need a child process; treating them like a Manager app made
2594
2594
  // "Open it" fail even though the HTML artifact was ready to view.
2595
2595
  if (!cmd && typeof url === 'string' && url.startsWith('/upload/')) {
2596
- return { url, reused: false, started: false };
2596
+ // The card opens this relative URL in a new tab. Carry the manager key in
2597
+ // that URL; otherwise the new navigation loses the authenticated fetch
2598
+ // context and the upload route correctly returns its 403 gate.
2599
+ const keyedUrl = `${url}${url.includes('?') ? '&' : '?'}key=${encodeURIComponent(ACCESS_KEY)}`;
2600
+ return { url: keyedUrl, reused: false, started: false };
2597
2601
  }
2598
2602
  const live = previews.get(task.id);
2599
2603
  if (live && live.proc.exitCode === null) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@galda/cli",
3
- "version": "0.10.78",
3
+ "version": "0.10.80",
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": {