@galda/cli 0.10.13 → 0.10.15

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
@@ -5792,7 +5792,7 @@ function bootDisconnected(info){
5792
5792
  + '<p>Galda runs on your own machine. Run this where you use Claude Code or Codex — this board comes alive the moment it connects, no refresh needed:</p>'
5793
5793
  + '<div class="cg-cmd"><code>npx @galda/cli</code><button class="cg-copy">Copy</button></div>'
5794
5794
  + '<p class="cg-who">Signed in as <b></b></p>'
5795
- + '<p class="cg-switch">Wrong account? <a href="/logout">Switch account →</a></p>'
5795
+ + '<p class="cg-switch">Already running Galda under a different account? Run <code>npx @galda/cli --signin</code> and pick this one. Or <a href="/logout">switch this browser →</a></p>'
5796
5796
  + '<div class="cg-wait"><i></i>WAITING FOR THIS DEVICE</div></div>';
5797
5797
  document.body.appendChild(ov);
5798
5798
  ov.querySelector('.cg-who b').textContent = email || 'your account';
@@ -5814,20 +5814,24 @@ function bootDisconnected(info){
5814
5814
  const cw = document.getElementById('composerWrap');
5815
5815
  if (cw) cw.addEventListener('mousedown', (e) => { if (e.target.closest('#appsel, #modelsel, #effortsel')) return; gate(e); });
5816
5816
 
5817
- // Come alive automatically: while this device's engine is offline the relay
5818
- // 503s every engine-backed path (only /, /theme.css, /fonts.css are served);
5819
- // the instant the agent connects, the same path is proxied to the live engine
5820
- // (200). So we probe an engine-only endpoint by STATUS and reload when it stops
5821
- // 503-ing. We must NOT decide this by fetching `/` and grepping its HTML for a
5822
- // sentinel: this very poll's source contains the string '__GALDA_DISCONNECTED',
5823
- // so it is baked into every proxied copy of the page — the match is ALWAYS
5824
- // "found", the reload never fires, and the tab stays stuck on the connect
5825
- // screen forever ("npx said done but nothing happens"). (fix: status, not body)
5826
- setInterval(() => {
5827
- fetch('/api/state', { cache: 'no-store', credentials: 'same-origin' })
5828
- .then((r) => { if (r.status !== 503) location.reload(); })
5829
- .catch(() => {});
5830
- }, 4000);
5817
+ // Come alive automatically WITHOUT polling-and-reloading (Masa 2026-07-16).
5818
+ // The relay pushes a 'connected' event over SSE the instant THIS account's agent
5819
+ // is STABLY online (debounced on the relay side), and we reload exactly once on
5820
+ // that edge. Event-driven + debounced makes a reload loop structurally
5821
+ // impossible: a flapping agent never reaches the stable-connect edge, so it
5822
+ // shows "reconnecting" instead of triggering reloads. This replaces the old
5823
+ // `setInterval` + `if (status !== 503) reload` poll, which amplified any agent
5824
+ // flap into an infinite reload (the ~10-failure "無限ループ").
5825
+ const cgWait = ov.querySelector('.cg-wait');
5826
+ try {
5827
+ const es = new EventSource('/presence');
5828
+ es.addEventListener('presence', (e) => {
5829
+ let d; try { d = JSON.parse(e.data); } catch { return; }
5830
+ if (d.state === 'connected') { es.close(); location.reload(); } // one reload, only on a stable connect
5831
+ else if (cgWait) cgWait.innerHTML = '<i></i>WAITING FOR THIS DEVICE';
5832
+ });
5833
+ window.addEventListener('beforeunload', () => { try { es.close(); } catch {} });
5834
+ } catch { /* no EventSource (ancient browser) → stay on the gate; never loops */ }
5831
5835
  }
5832
5836
 
5833
5837
  async function boot(){
package/engine/server.mjs CHANGED
@@ -20,8 +20,8 @@ import { resolve, dirname, join, basename } from 'node:path';
20
20
  import { homedir } from 'node:os';
21
21
  import { fileURLToPath } from 'node:url';
22
22
  import { pathToFileURL } from 'node:url';
23
- import { needsProjectFolder, folderLabel, buildFolderQuestion, resolveFolderAnswer, needsReviewPreference, buildReviewPreferenceQuestion, resolveReviewPreferenceAnswer } from './lib.mjs';
24
- import { parseStreamEvents, parsePlan, refinePlanTasks, findOverlappingGoal, canEditGoal, canDeleteGoal, shouldAskClarification, workerExitReason, isForcedStop, workerResultText, taskCountsAsComplete, resolveWantsPR, resolveGoalSource, buildGoalSummary, buildGoalProofMd, buildGoalPrBody, nextGoalStatus, isPrMerged, isPrApproved, reviewToDoneStatus, advanceReviewGoal, revertReviewGoal, approveGoal, dismissGoal, 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, verifyCfAccessJwt, resolveIdentity, goalVisibleTo, clampParallelLimit, canStartMore, nextRunnableTasks, goalsConflict, detectConflicts, isUiChange, shouldCaptureProof, shouldCaptureBaseline, hasTestRelevantChanges, buildExecutionPlan, buildContextHandoffSummary, buildFailurePostmortem, appendFailureMemory, latestFailurePolicy, classifyChangeRisk, checkRunBudget, usageBudgetTokens, isRateLimited, nextResumeDelay, checkFreeTierLimit, resolveEntitlement, resolveCachedEntitlement, FREE_TIER_LIMITS, verifyLicenseToken, shouldEmitSetupCompleted, pickAnalyticsUid, shouldEmitFreeExhausted, detectRequestLanguage, isNothingVerifiable } from './lib.mjs';
23
+ import { needsProjectFolder, folderLabel, buildFolderQuestion, resolveFolderAnswer } from './lib.mjs';
24
+ import { parseStreamEvents, parsePlan, refinePlanTasks, findOverlappingGoal, canEditGoal, canDeleteGoal, shouldAskClarification, workerExitReason, isForcedStop, workerResultText, taskCountsAsComplete, resolveWantsPR, resolveGoalSource, buildGoalSummary, buildGoalProofMd, buildReviewRuleSection, nextGoalStatus, isPrMerged, isPrApproved, reviewToDoneStatus, advanceReviewGoal, revertReviewGoal, approveGoal, dismissGoal, 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, verifyCfAccessJwt, resolveIdentity, goalVisibleTo, clampParallelLimit, canStartMore, nextRunnableTasks, goalsConflict, detectConflicts, isUiChange, shouldCaptureProof, shouldCaptureBaseline, hasTestRelevantChanges, buildExecutionPlan, buildContextHandoffSummary, buildFailurePostmortem, appendFailureMemory, latestFailurePolicy, classifyChangeRisk, checkRunBudget, usageBudgetTokens, isRateLimited, nextResumeDelay, checkFreeTierLimit, resolveEntitlement, resolveCachedEntitlement, FREE_TIER_LIMITS, verifyLicenseToken, shouldEmitSetupCompleted, pickAnalyticsUid, shouldEmitFreeExhausted, detectRequestLanguage, isNothingVerifiable } from './lib.mjs';
25
25
  import { openPR } from './pr.mjs';
26
26
  import { runVerification, exerciseUi } from './verify.mjs';
27
27
 
@@ -2160,11 +2160,6 @@ async function finishGoalIfComplete(goal, project, activityTask = null) {
2160
2160
  // point (all its tasks are done), so its conflictsWith is accurate even if
2161
2161
  // PR creation itself fails (e.g. no `gh` auth) or is still no-op'd (no code).
2162
2162
  updateGoalConflicts(goal);
2163
- // Computed BEFORE createGoalPR (not inside verifyGate, which used to run
2164
- // after) so the PR body's "確認ポイント" headline is available the first
2165
- // time the PR is opened, not only on the review card (Masa 2026-07-16).
2166
- goalAct('Writing the review summary from the worker report and diff.');
2167
- goal.reviewSummary = await summarizeForReview(goal, siblings, wdir, reviewDefinition);
2168
2163
  goalAct('Checking changed files and preparing the PR branch if code changed.');
2169
2164
  await createGoalPR(goal, prProject, siblings, reviewDefinition, activityTask);
2170
2165
  // goal 427 §4: classify the change's risk from the SAME changed-files/diff
@@ -2233,13 +2228,8 @@ async function verifyGate(goal, prProject, siblings, baseProject, activityTask =
2233
2228
  goal.testResult = { ran: false, skipped: true, skippedReason: changedFiles.length ? 'documentation-only change' : 'no changed files' };
2234
2229
  goalAct(`Skipped project tests: ${goal.testResult.skippedReason}.`);
2235
2230
  }
2236
- // Normally already set by finishGoalIfComplete before the PR was opened
2237
- // (so the PR body can include it too); only fall back here for the
2238
- // standalone /reverify path or any older goal that never got one.
2239
- if (!goal.reviewSummary) {
2240
- goalAct('Writing the review summary from the worker report and diff.');
2241
- goal.reviewSummary = await summarizeForReview(goal, siblings, wdir, reviewDefinition);
2242
- }
2231
+ goalAct('Writing the review summary from the worker report and diff.');
2232
+ goal.reviewSummary = await summarizeForReview(goal, siblings, wdir, reviewDefinition);
2243
2233
  const uiTasks = siblings.filter((t) => isUiChange(t.changedFiles));
2244
2234
  // Only require/capture proof for projects we can actually boot & screenshot
2245
2235
  // (Manager-shaped). A generic web repo that edits a stylesheet can't be
@@ -2381,12 +2371,26 @@ async function createGoalPR(goal, project, goalTasks, reviewDefinition = DEFAULT
2381
2371
  // (Masa: 「ユーザーの言語で」).
2382
2372
  const lang = reviewDefinition.language === 'auto' ? detectRequestLanguage(goal.text) : (reviewDefinition.language || 'ja');
2383
2373
  const L = (en, ja) => (lang === 'en' ? en : ja);
2384
- const bodyFor = (branch) => buildGoalPrBody({
2385
- lang, goal, goalTasks, files, owner, branch, proofRel,
2386
- proofItemsCount: proofItems.length,
2387
- reviewDescription: reviewDefinition.description,
2388
- reviewSummary: goal.reviewSummary,
2389
- });
2374
+ const bodyFor = (branch) => [
2375
+ ...(proofItems.length && owner ? [
2376
+ L('## Proof — one tap, plays in place', '## 動作確認 — 1タップでその場で再生'),
2377
+ '',
2378
+ `https://github.com/${owner}/blob/${branch}/${proofRel}/PROOF.md`,
2379
+ '',
2380
+ ] : []),
2381
+ L('## Goal', '## ゴール'),
2382
+ '',
2383
+ goal.text.slice(0, 1500),
2384
+ '',
2385
+ ...buildReviewRuleSection(reviewDefinition.description),
2386
+ L(`Implemented by Manager for AI: the goal was decomposed into ${goalTasks.length} task(s), each executed by a Claude Code worker (flat-rate, no extra API), tests written and run per task.${proofItems.length ? ' Tasks with a pass condition were verified independently in a headless browser.' : ''}`,
2387
+ `Manager for AI が実装: ゴールを${goalTasks.length}個のタスクに分解し、Claude Code worker が順に実行(既存サブスク・追加API無し・タスク毎にテストを作成し実行)。${proofItems.length ? '成功条件つきタスクは headless ブラウザで独立検証済み。' : ''}`),
2388
+ '',
2389
+ L('## Tasks and worker reports', '## タスクとworker報告'),
2390
+ ...goalTasks.map((t) => `\n### ${t.num} ${t.title} — ${t.status} (${t.secs}s)\n${(t.result ?? '').slice(0, 800)}`),
2391
+ '',
2392
+ L(`Changed files: ${files.map((f) => `\`${f}\``).join(' ')}`, `変更ファイル: ${files.map((f) => `\`${f}\``).join(' ')}`),
2393
+ ].join('\n');
2390
2394
  const title = `Manager for AI: ${isRework ? L('rework — ', '再作業 — ') : ''}${goal.plan?.[0] ?? goal.text.slice(0, 60)}${goal.plan?.length > 1 ? L(` +${goal.plan.length - 1} more`, ` 他${goal.plan.length - 1}件`) : ''}`;
2391
2395
  const { url, branch } = openPR({
2392
2396
  repoDir: project.dir, slug: `goal-${goal.id}`,
@@ -2397,7 +2401,7 @@ async function createGoalPR(goal, project, goalTasks, reviewDefinition = DEFAULT
2397
2401
  extraCopies,
2398
2402
  extraWrites: proofItems.length ? [{
2399
2403
  destRel: `${proofRel}/PROOF.md`,
2400
- content: buildGoalProofMd({ goalText: goal.text, items: proofItems, lang }),
2404
+ content: buildGoalProofMd({ goalText: goal.text, items: proofItems }),
2401
2405
  }] : [],
2402
2406
  ...(isRework ? { existingBranch: goal.prBranch, existingPrUrl: goal.pr } : {}),
2403
2407
  });
@@ -2746,22 +2750,15 @@ const server = createServer(async (req, res) => {
2746
2750
  // for pending/review items (they never spawn a worker).
2747
2751
  const runnable = !pending && !review;
2748
2752
  const needsFolder = runnable && needsProjectFolder({ dir: project.dir, homeDir: HOME, isRepo: isGitRepo(project.dir) });
2749
- const lang = /[぀-ヿ㐀-鿿]/.test(text) ? 'ja' : 'en';
2750
- const folderQuestion = needsFolder ? buildFolderQuestion(detectRepos(HOME), { lang }) : undefined;
2751
- // Task 5 (PR運用ヒアリング): once the folder is settled, ask — once per
2752
- // project, before its first runnable goal — whether this project wants
2753
- // PRs. Skipped this round if a folder question already claimed the
2754
- // needsInput slot; that goal gets rebound to a real repo via /answer
2755
- // and the NEXT goal for that project is what actually asks this.
2756
- const needsReviewPref = runnable && !needsFolder && needsReviewPreference({ askedBefore: project.reviewPrefAsked });
2757
- const reviewPrefQuestion = needsReviewPref ? buildReviewPreferenceQuestion({ lang }) : undefined;
2758
- if (needsReviewPref) { project.reviewPrefAsked = true; saveProjects(); }
2753
+ const folderQuestion = needsFolder
2754
+ ? buildFolderQuestion(detectRepos(HOME), { lang: /[぀-ヿ㐀-鿿]/.test(text) ? 'ja' : 'en' })
2755
+ : undefined;
2759
2756
  const goalAgent = workerAgent(agent);
2760
2757
  const priorFailureMemory = recentProjectFailureMemory(projectId, text.trim());
2761
2758
  const goal = {
2762
2759
  id: nextId++, projectId, text: text.trim().slice(0, 8000),
2763
- status: (needsFolder || needsReviewPref) ? 'needsInput' : review ? 'review' : pending ? 'pending' : 'planning',
2764
- question: folderQuestion || reviewPrefQuestion,
2760
+ status: needsFolder ? 'needsInput' : review ? 'review' : pending ? 'pending' : 'planning',
2761
+ question: folderQuestion,
2765
2762
  reviewOnly: review ? true : undefined,
2766
2763
  note: review && typeof note === 'string' && note.trim() ? note.trim().slice(0, 8000) : undefined,
2767
2764
  wantsPR: review ? false : resolveWantsPR(pr, text, getReviewDefinition(projectId).defaultWantsPR),
@@ -2791,7 +2788,7 @@ const server = createServer(async (req, res) => {
2791
2788
  // pending = deliberately shelved (Phase 2 material): never planned,
2792
2789
  // never queued, until POST /api/goals/:id/activate. review-only items
2793
2790
  // are terminal-until-human too, so they also skip planGoal.
2794
- if (runnable && !needsFolder && !needsReviewPref) {
2791
+ if (runnable && !needsFolder) {
2795
2792
  planGoal(goal).catch((e) => {
2796
2793
  goal.status = 'failed'; goal.prError = String(e).slice(0, 300); saveGoal(goal);
2797
2794
  });
@@ -3118,28 +3115,6 @@ const server = createServer(async (req, res) => {
3118
3115
  saveGoal(goal);
3119
3116
  return json(res, 200, goal);
3120
3117
  }
3121
- // Review-preference answer (task 5 onboarding funnel): fold into the
3122
- // project's review definition (defaultWantsPR) rather than goal.text —
3123
- // like the folder question, this is project setup, not a task detail.
3124
- if (goal.question?.kind === 'reviewPref') {
3125
- const project = projects.find((p) => p.id === goal.projectId);
3126
- const patch = resolveReviewPreferenceAnswer(answer, goal.question);
3127
- if (project) {
3128
- const merged = validateReviewDefinition({ ...getReviewDefinition(project.id), ...patch });
3129
- if (merged.ok) {
3130
- reviewDefinitions[project.id] = merged.definition;
3131
- saveReviewDefinitions();
3132
- send({ ev: 'review-definition', projectId: project.id, reviewDefinition: merged.definition });
3133
- }
3134
- }
3135
- goal.wantsPR = resolveWantsPR(undefined, goal.text, patch.defaultWantsPR);
3136
- goal.question = null;
3137
- goal.clarified = true;
3138
- goal.status = 'planning';
3139
- saveGoal(goal);
3140
- planGoal(goal).catch((e) => { goal.status = 'failed'; goal.prError = String(e).slice(0, 300); saveGoal(goal); });
3141
- return json(res, 200, goal);
3142
- }
3143
3118
  goal.text = `${goal.text}\n[確認: ${goal.question?.text ?? ''} → ${answer}]`;
3144
3119
  goal.clarified = true; // answered once → planGoal must never re-ask (persisted via saveGoal)
3145
3120
  goal.question = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@galda/cli",
3
- "version": "0.10.13",
3
+ "version": "0.10.15",
4
4
  "type": "module",
5
5
  "description": "Galda - hand off work to your Claude Code, get proof back. Runs on your existing subscription, no extra API cost.",
6
6
  "scripts": {
@@ -1,20 +0,0 @@
1
- {
2
- "task": {
3
- "id": "task-001",
4
- "title": "Save button toast",
5
- "passCondition": "click the #save button, then an element matching .toast becomes visible with text containing 'Saved' within 2 seconds"
6
- },
7
- "status": "done",
8
- "attempts": [
9
- {
10
- "attempt": 1,
11
- "workerSecs": "21.7",
12
- "pass": true,
13
- "detail": "clicked #save → toast \"Saved!\" visible",
14
- "proofScreenshot": "/Users/masakinakata/common/project/galda2/tasks/task-001/runs/2026-07-03T06-23-57-441Z/attempt-1-proof.png",
15
- "proofVideo": "/Users/masakinakata/common/project/galda2/tasks/task-001/runs/2026-07-03T06-23-57-441Z/attempt-1.webm"
16
- }
17
- ],
18
- "runDir": "/Users/masakinakata/common/project/galda2/tasks/task-001/runs/2026-07-03T06-23-57-441Z",
19
- "finishedAt": "2026-07-03T06:24:22.279Z"
20
- }
@@ -1,20 +0,0 @@
1
- {
2
- "task": {
3
- "id": "task-001",
4
- "title": "Save button toast",
5
- "passCondition": "click the #save button, then an element matching .toast becomes visible with text containing 'Saved' within 2 seconds"
6
- },
7
- "status": "done",
8
- "attempts": [
9
- {
10
- "attempt": 1,
11
- "workerSecs": "19.8",
12
- "pass": true,
13
- "detail": "clicked #save → toast \"Saved!\" visible",
14
- "proofScreenshot": "/Users/masakinakata/common/project/galda2/tasks/task-001/runs/2026-07-03T06-29-54-517Z/attempt-1-proof.png",
15
- "proofVideo": "/Users/masakinakata/common/project/galda2/tasks/task-001/runs/2026-07-03T06-29-54-517Z/attempt-1.webm"
16
- }
17
- ],
18
- "runDir": "/Users/masakinakata/common/project/galda2/tasks/task-001/runs/2026-07-03T06-29-54-517Z",
19
- "finishedAt": "2026-07-03T06:30:17.155Z"
20
- }
@@ -1,20 +0,0 @@
1
- {
2
- "task": {
3
- "id": "task-001",
4
- "title": "Save button toast",
5
- "passCondition": "click the #save button, then an element matching .toast becomes visible with text containing 'Saved' within 2 seconds"
6
- },
7
- "status": "done",
8
- "attempts": [
9
- {
10
- "attempt": 1,
11
- "workerSecs": "18.2",
12
- "pass": true,
13
- "detail": "clicked #save → toast \"Saved!\" visible",
14
- "proofScreenshot": "/Users/masakinakata/common/project/galda2/tasks/task-001/runs/2026-07-03T06-40-23-231Z/attempt-1-proof.png",
15
- "proofVideo": "/Users/masakinakata/common/project/galda2/tasks/task-001/runs/2026-07-03T06-40-23-231Z/attempt-1.webm"
16
- }
17
- ],
18
- "runDir": "/Users/masakinakata/common/project/galda2/tasks/task-001/runs/2026-07-03T06-40-23-231Z",
19
- "finishedAt": "2026-07-03T06:40:46.785Z"
20
- }
@@ -1,21 +0,0 @@
1
- {
2
- "task": {
3
- "id": "task-001",
4
- "title": "Save button toast",
5
- "passCondition": "click the #save button, then an element matching .toast becomes visible with text containing 'Saved' within 2 seconds"
6
- },
7
- "status": "done",
8
- "attempts": [
9
- {
10
- "attempt": 1,
11
- "workerSecs": "22.3",
12
- "pass": true,
13
- "detail": "clicked #save → toast \"Saved!\" visible",
14
- "proofScreenshot": "/Users/masakinakata/common/project/galda2/tasks/task-001/runs/2026-07-03T07-34-58-109Z/attempt-1-proof.png",
15
- "proofVideo": "/Users/masakinakata/common/project/galda2/tasks/task-001/runs/2026-07-03T07-34-58-109Z/attempt-1.mp4"
16
- }
17
- ],
18
- "runDir": "/Users/masakinakata/common/project/galda2/tasks/task-001/runs/2026-07-03T07-34-58-109Z",
19
- "finishedAt": "2026-07-03T07:35:25.892Z",
20
- "pr": "https://github.com/kodo-inc/common/pull/1"
21
- }
@@ -1,21 +0,0 @@
1
- {
2
- "task": {
3
- "id": "task-001",
4
- "title": "Save button toast",
5
- "passCondition": "click the #save button, then an element matching .toast becomes visible with text containing 'Saved' within 2 seconds"
6
- },
7
- "status": "done",
8
- "attempts": [
9
- {
10
- "attempt": 1,
11
- "workerSecs": "21.9",
12
- "pass": true,
13
- "detail": "clicked #save → toast \"Saved!\" visible",
14
- "proofScreenshot": "/Users/masakinakata/galda2/tasks/task-001/runs/2026-07-03T07-53-44-639Z/attempt-1-proof.png",
15
- "proofVideo": "/Users/masakinakata/galda2/tasks/task-001/runs/2026-07-03T07-53-44-639Z/attempt-1.mp4"
16
- }
17
- ],
18
- "runDir": "/Users/masakinakata/galda2/tasks/task-001/runs/2026-07-03T07-53-44-639Z",
19
- "finishedAt": "2026-07-03T07:54:11.642Z",
20
- "pr": "https://github.com/kodo-inc/galda2/pull/1"
21
- }
@@ -1,21 +0,0 @@
1
- {
2
- "task": {
3
- "id": "task-001",
4
- "title": "Save button toast",
5
- "passCondition": "click the #save button, then an element matching .toast becomes visible with text containing 'Saved' within 2 seconds"
6
- },
7
- "status": "done",
8
- "attempts": [
9
- {
10
- "attempt": 1,
11
- "workerSecs": "16.3",
12
- "pass": true,
13
- "detail": "clicked #save → toast \"Saved!\" visible",
14
- "proofScreenshot": "/Users/masakinakata/galda2/tasks/task-001/runs/2026-07-03T08-02-12-117Z/attempt-1-proof.png",
15
- "proofVideo": "/Users/masakinakata/galda2/tasks/task-001/runs/2026-07-03T08-02-12-117Z/attempt-1.mp4"
16
- }
17
- ],
18
- "runDir": "/Users/masakinakata/galda2/tasks/task-001/runs/2026-07-03T08-02-12-117Z",
19
- "finishedAt": "2026-07-03T08:02:33.935Z",
20
- "pr": "https://github.com/kodo-inc/galda2/pull/2"
21
- }