@galda/cli 0.10.79 → 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
@@ -6828,7 +6828,10 @@ function renderFsBoard(){
6828
6828
  // errored is status-based now (blocked/failed/partial/interrupted live in the Needs-you column,
6829
6829
  // not Doing). Doing holds only what's actively moving, so bkt==='doing' ⇒ healthy in flight.
6830
6830
  const errored = ['blocked', 'failed', 'partial', 'interrupted'].includes(g.status);
6831
- 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');
6832
6835
  const isPaused = g.status === 'blocked' && g.blocked?.kind === 'paused'; // 一時停止=エラーでなくアンバー扱い
6833
6836
  const amber = intr || isPaused;
6834
6837
  const healthyRun = bkt === 'doing' && !errored; // paused(=errored) goals now sit in 'doing' too but carry no progress bar
@@ -7161,7 +7164,10 @@ function errorButtonsHtml(meta, { taskId = null, goalId = null, interrupted = fa
7161
7164
  const out = [];
7162
7165
  for (const id of meta.actionIds || []) {
7163
7166
  if (id === 'retry' && taskId != null) out.push(`<button class="errbtn" data-fsretry="${taskId}">${interrupted ? C.resume : C.retry}</button>`);
7164
- 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>`);
7165
7171
  else if (id === 'connect') out.push(`<button class="errbtn" data-fsconnectworker="1">${C.connect}</button>`);
7166
7172
  else if (id === 'check-connection') out.push(`<button class="errbtn" data-fscheckworker="1">${C.check}</button>`);
7167
7173
  else if (id === 'retry-tests' && goalId != null) out.push(`<button class="errbtn" data-fsreverify="${goalId}">${C.tests}</button>`);
@@ -7297,10 +7303,6 @@ function renderFsTodo(){
7297
7303
  const needsCount = failed.filter((t) => !resumedGoalIds.has(t.goalId)).length + blocked.length + needsInput.length;
7298
7304
  const doingCount = running.length + finalizingGoals.length + pausedGoalIds.size;
7299
7305
  const nextCount = parkedReplyGoals.length + queued.length;
7300
- const hasPipelineWork = doingCount > 0 || nextCount > 0 || pgoals.some((g) => ['planning', 'stacked'].includes(g.status));
7301
- const laterIdle = later.length && !hasPipelineWork
7302
- ? `<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>`
7303
- : '';
7304
7306
  // Free-tier pending counter (H22): on the free plan the header shows the wall
7305
7307
  // — pending goals out of the 5-slot cap — reddening at the cap and gone once
7306
7308
  // paid. Reads the SAME numbers the gate enforces (state.billing, never a local
@@ -7322,7 +7324,6 @@ function renderFsTodo(){
7322
7324
  <div class="qadd" data-fsnew="1" role="button" tabindex="0" title="New goal"><span class="paplus"></span><span>New</span></div>
7323
7325
  <span class="tlcap">Later</span>
7324
7326
  <div class="qadd lateradd" data-fslaternew="1" role="button" tabindex="0" title="New Later goal"><span class="paplus"></span><span>New</span></div>
7325
- ${laterIdle}
7326
7327
  ${laterRows}
7327
7328
  </div>
7328
7329
  ${doneAll.length ? `<div class="doneline${fsUI.doneOpen ? ' open' : ''}" id="fsDoneLine">✓ Done<span class="num">${doneAll.length}</span><span class="fold">▾</span></div>
@@ -7387,13 +7388,6 @@ function renderFsTodo(){
7387
7388
  let r; try { r = await fetch(withKey(`/api/goals/${b.dataset.fspstart}/activate`), { method: 'POST' }); }
7388
7389
  catch { showErr('Start failed.'); return; }
7389
7390
  if (!r.ok) showErr('Start failed.'); else await refresh(); };
7390
- const startNextLater = el.querySelector('[data-fsstartnextlater]');
7391
- if (startNextLater) startNextLater.onclick = async (e) => { e.stopPropagation();
7392
- startNextLater.disabled = true;
7393
- let r; try { r = await fetch(withKey(`/api/goals/${startNextLater.dataset.fsstartnextlater}/activate`), { method: 'POST' }); }
7394
- catch { showErr(document.documentElement.lang === 'ja' ? '開始できませんでした' : 'Could not start the next item.'); startNextLater.disabled = false; return; }
7395
- if (!r.ok) { showErr(document.documentElement.lang === 'ja' ? '開始できませんでした' : 'Could not start the next item.'); startNextLater.disabled = false; }
7396
- else await refresh(); };
7397
7391
  for (const b of el.querySelectorAll('[data-fspdel]')) b.onclick = async (e) => { e.stopPropagation();
7398
7392
  const preview = b.closest('.trow')?.querySelector('.tt')?.textContent || '';
7399
7393
  if (!(await confirmDelete({ title: 'Delete this shelved goal?', preview }))) return;
@@ -8933,7 +8927,14 @@ async function send(){
8933
8927
  // review goal → the AI decides continue/rescope/split/answer (same path as the
8934
8928
  // review card's own reply box), with the note anchored under the composer.
8935
8929
  const body = await replyToGoal(bind.goal.id, fullText, '#input');
8936
- 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
+ }
8937
8938
  } else {
8938
8939
  // running/partial/failed/interrupted/blocked → a plain follow-up on the same goal.
8939
8940
  const r = await fetch(withKey(`/api/goals/${bind.goal.id}/reply`), { method: 'POST',
@@ -9178,6 +9179,11 @@ function latestGoalRunForOutcome(goalId, tasks, outcome){
9178
9179
  function saRowFromDigest(r, gs, ts){
9179
9180
  const g = gs.find((x) => x.id === r.goalIds[0]);
9180
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;
9181
9187
  const pt = ts.find((t) => r.goalIds.includes(t.goalId) && t.proof && (t.proof.png || t.proof.gif));
9182
9188
  // "What we did" (H23 §2, replaces the old Plan/Review-thread folds): one bullet
9183
9189
  // per finished requirement task's own report; falls back to the goal's plan
@@ -9207,6 +9213,7 @@ function saRowFromDigest(r, gs, ts){
9207
9213
  .map((x) => ({ id: x.id, title: saReviewTitle(x) }));
9208
9214
  return {
9209
9215
  ids: r.goalIds, goalId: r.goalIds[0], goal: g, folded,
9216
+ interruptedTask,
9210
9217
  no: r.reviewNumber,
9211
9218
  title: saReviewTitle(g),
9212
9219
  check: String(g?.reviewSummary?.check || r.checkLine || '').replace(/\s+/g, ' ').trim(),
@@ -9733,10 +9740,27 @@ function saItemHtml(it, i){
9733
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>`
9734
9741
  : '';
9735
9742
  const openBody = (it.run?.cmd || it.run?.url)
9736
- ? `<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>`
9737
9744
  : it.pr
9738
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>`
9739
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
+ })() : '';
9740
9764
  const prRepairBody = it.goal?.prError
9741
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>`
9742
9766
  : '';
@@ -9899,8 +9923,8 @@ function saItemHtml(it, i){
9899
9923
  const attn = !working && ['blocked', 'partial', 'failed', 'interrupted'].includes(gstatus);
9900
9924
  // Match the flagship list's state colours (Masa's Interrupt=amber / Failed=red rule): an
9901
9925
  // interrupted goal is amber+resumable, everything else stopped is a red error.
9902
- const attnInt = gstatus === 'interrupted';
9903
- 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';
9904
9928
  const rst = working
9905
9929
  ? '<span class="rst working"><span class="rdot"></span>Working on it</span>'
9906
9930
  : attn
@@ -9912,6 +9936,7 @@ function saItemHtml(it, i){
9912
9936
  ${meta}
9913
9937
  ${reqSec}
9914
9938
  ${S('Open it', openBody)}
9939
+ ${S('Run state', interruptedBody)}
9915
9940
  ${S('PR', prRepairBody)}
9916
9941
  ${S('Also carries', carriesBody)}
9917
9942
  ${S('What to check', checkBody)}
@@ -10396,6 +10421,11 @@ $('seeall').addEventListener('click', async (e) => {
10396
10421
  }
10397
10422
  const it = SA.items[i];
10398
10423
  if (!it) return;
10424
+ if (k === 'resume') {
10425
+ const taskId = it.interruptedTask?.id;
10426
+ if (taskId != null) return fsRetryTask(taskId);
10427
+ return;
10428
+ }
10399
10429
  if (k === 'retrypr') return fsRetryPrGoal({ disabled: false, textContent: '', dataset: { fsprretry: String(it.goalId) } });
10400
10430
  if (k === 'glog') return toggleGoalDetail(it.goalId);
10401
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.79",
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": {