@loicngr/kobo 1.11.35 → 1.11.37

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.
Files changed (102) hide show
  1. package/AGENTS.md +15 -0
  2. package/CHANGELOG.md +9 -0
  3. package/dist/server/index.js +11 -24
  4. package/dist/server/routes/dev-server.js +2 -1
  5. package/dist/server/routes/pull-requests.js +5 -4
  6. package/dist/server/routes/workspaces.js +122 -91
  7. package/dist/server/services/agent/engines/claude-code/engine.js +26 -0
  8. package/dist/server/services/agent/engines/claude-code/event-mapper.js +10 -10
  9. package/dist/server/services/agent/engines/codex/client.js +1 -0
  10. package/dist/server/services/agent/engines/codex/engine.js +47 -10
  11. package/dist/server/services/agent/engines/codex/event-mapper.js +5 -1
  12. package/dist/server/services/agent/engines/codex/jsonrpc/peer.js +21 -8
  13. package/dist/server/services/agent/engines/codex/jsonrpc/transport.js +30 -3
  14. package/dist/server/services/agent/orchestrator.js +175 -68
  15. package/dist/server/services/agent/session-controller.js +12 -2
  16. package/dist/server/services/auto-loop-service.js +71 -6
  17. package/dist/server/services/change-source-branch-service.js +36 -4
  18. package/dist/server/services/cron-service.js +19 -0
  19. package/dist/server/services/db-backup-service.js +30 -0
  20. package/dist/server/services/dev-server-service.js +167 -38
  21. package/dist/server/services/image-service.js +25 -28
  22. package/dist/server/services/pr-checkout-service.js +34 -5
  23. package/dist/server/services/quota-backoff-service.js +25 -8
  24. package/dist/server/services/wakeup-service.js +37 -3
  25. package/dist/server/services/worktree-purge-service.js +3 -7
  26. package/dist/server/utils/agent-stop-result.js +12 -0
  27. package/dist/server/utils/git-ops.js +8 -1
  28. package/dist/server/utils/git-worktree-fingerprint.js +86 -0
  29. package/dist/server/utils/safe-path.js +21 -0
  30. package/dist/server/utils/script-runner.js +32 -9
  31. package/dist/server/utils/workspace-lifecycle-guard.js +27 -0
  32. package/package.json +1 -1
  33. package/src/client/dist/pwa/assets/{ActivityFeed-Bakgvay6.css → ActivityFeed-BDyFBoxd.css} +1 -1
  34. package/src/client/dist/pwa/assets/ActivityFeed-C_-hxIo8.js +8 -0
  35. package/src/client/dist/pwa/assets/{ChangelogPage-DVLWWvvz.js → ChangelogPage-CBlGJ4zT.js} +1 -1
  36. package/src/client/dist/pwa/assets/{ComparisonPanel-LZrvk6a6.js → ComparisonPanel-DZAE2X9j.js} +1 -1
  37. package/src/client/dist/pwa/assets/CreatePage-A3pVradx.css +1 -0
  38. package/src/client/dist/pwa/assets/CreatePage-ArmtcF4d.js +2 -0
  39. package/src/client/dist/pwa/assets/{DashboardPage-0odIhseb.js → DashboardPage-eQTCXcdz.js} +1 -1
  40. package/src/client/dist/pwa/assets/{DiffViewer-NWB2uQkz.js → DiffViewer-CaT7vuSr.js} +3 -3
  41. package/src/client/dist/pwa/assets/{DocumentsPanel-3yApHgbl.js → DocumentsPanel-A0isCTaI.js} +1 -1
  42. package/src/client/dist/pwa/assets/{DrawerToggleButton-C_bNtZsE.js → DrawerToggleButton-T70fh8dp.js} +1 -1
  43. package/src/client/dist/pwa/assets/{GitPanel-BMjWwiAX.js → GitPanel-CAvk0G7h.js} +3 -3
  44. package/src/client/dist/pwa/assets/{HealthPage-Vyr1YG2o.js → HealthPage-D8Mr0V6w.js} +1 -1
  45. package/src/client/dist/pwa/assets/{MainLayout-s8_9FPE2.js → MainLayout-x6Es4Da-.js} +2 -2
  46. package/src/client/dist/pwa/assets/{QBtnDropdown-BxuXEBp5.js → QBtnDropdown-CQqzzlFI.js} +1 -1
  47. package/src/client/dist/pwa/assets/{QDrawer-BdlDiU6a.js → QDrawer-Cwj2qh0r.js} +1 -1
  48. package/src/client/dist/pwa/assets/{QSelect-BrSemS3o.js → QSelect-BSPtuXJg.js} +1 -1
  49. package/src/client/dist/pwa/assets/{QTooltip-CLCFHW2D.js → QTooltip-DC_kDKku.js} +1 -1
  50. package/src/client/dist/pwa/assets/{SchedulePanel-F9bdAq6K.js → SchedulePanel-9aJSoz6Y.js} +1 -1
  51. package/src/client/dist/pwa/assets/{SearchPage-CBCefYqc.js → SearchPage-BjNvtE_U.js} +1 -1
  52. package/src/client/dist/pwa/assets/{SettingsPage-BPgTHmKj.js → SettingsPage-CvLnT73V.js} +1 -1
  53. package/src/client/dist/pwa/assets/{SplitWorkspacePage-DDs2nuoT.js → SplitWorkspacePage-DTao0DHr.js} +1 -1
  54. package/src/client/dist/pwa/assets/{TerminalPanel-CulE0wTU.js → TerminalPanel-BrKIW-bJ.js} +1 -1
  55. package/src/client/dist/pwa/assets/{TouchPan-DUgqs2cW.js → TouchPan-_n1AQgoo.js} +1 -1
  56. package/src/client/dist/pwa/assets/{TourReplayButton-CdHpGBdr.js → TourReplayButton-CJF3nVvn.js} +1 -1
  57. package/src/client/dist/pwa/assets/{WorkspaceAttentionLabels-W5SvqzXO.js → WorkspaceAttentionLabels-CxesUiIm.js} +1 -1
  58. package/src/client/dist/pwa/assets/{WorkspacePage-DYQrfeYw.css → WorkspacePage-DoXtwpjV.css} +1 -1
  59. package/src/client/dist/pwa/assets/WorkspacePage-MxBE0mr3.js +4 -0
  60. package/src/client/dist/pwa/assets/{browser-CnuqDTyc.js → browser-DGWpkvN3.js} +1 -1
  61. package/src/client/dist/pwa/assets/{build-path-tree-JodIO7-I.js → build-path-tree-Ba1dSLgi.js} +1 -1
  62. package/src/client/dist/pwa/assets/{de-Dt_m-H83.js → de-DVK21RB6.js} +1 -1
  63. package/src/client/dist/pwa/assets/{documents-3ZBK1pXE.js → documents-BMHzjW_g.js} +1 -1
  64. package/src/client/dist/pwa/assets/{editor.api-DZ9WO_J7.js → editor.api-CYTuNwqu.js} +1 -1
  65. package/src/client/dist/pwa/assets/{efforts-LrtJPx-j.js → efforts-BPlRSHXp.js} +1 -1
  66. package/src/client/dist/pwa/assets/{engineFeatures-DmnHj_uD.js → engineFeatures-DVB2rklO.js} +1 -1
  67. package/src/client/dist/pwa/assets/{es-DFg25DCS.js → es-C8cQwDZM.js} +1 -1
  68. package/src/client/dist/pwa/assets/{expand-template-Dv4JC2zU.js → expand-template-Cj82YE-O.js} +1 -1
  69. package/src/client/dist/pwa/assets/{format-CHjfjuhr.js → format-Dk7O3LhA.js} +1 -1
  70. package/src/client/dist/pwa/assets/{formatters-BsgH8CoH.js → formatters-TGjvs7Ca.js} +1 -1
  71. package/src/client/dist/pwa/assets/{fr-CQ1slpol.js → fr-BVOMZ2Bl.js} +1 -1
  72. package/src/client/dist/pwa/assets/{freemarker2-Dla55SIR.js → freemarker2-39lO_dWH.js} +1 -1
  73. package/src/client/dist/pwa/assets/{handlebars-DtuuZy7D.js → handlebars-M_0V82cD.js} +1 -1
  74. package/src/client/dist/pwa/assets/{html-CKYOCR8W.js → html-yILqYDsl.js} +1 -1
  75. package/src/client/dist/pwa/assets/i18n-2UeMfd5m.js +1 -0
  76. package/src/client/dist/pwa/assets/{index-CIP08MpU.js → index-BJU5dnkF.js} +2 -2
  77. package/src/client/dist/pwa/assets/{it-w09rfaP7.js → it-CxkAoOj4.js} +1 -1
  78. package/src/client/dist/pwa/assets/{javascript-BxzTnsIQ.js → javascript-DrW-amDc.js} +1 -1
  79. package/src/client/dist/pwa/assets/{liquid-C8BfPFUy.js → liquid-D_mXmUoL.js} +1 -1
  80. package/src/client/dist/pwa/assets/{mdx-DZm_YXkC.js → mdx-E8O3lvR5.js} +1 -1
  81. package/src/client/dist/pwa/assets/{monaco-contributions-HDbRHiNR.js → monaco-contributions-C2K3sl_V.js} +1 -1
  82. package/src/client/dist/pwa/assets/{monaco.contribution-DRW-851R.js → monaco.contribution-DLhHQIJn.js} +2 -2
  83. package/src/client/dist/pwa/assets/{network-auth-Ce9hsryo.js → network-auth-BUwWtnui.js} +1 -1
  84. package/src/client/dist/pwa/assets/{notify-theme-OBpMEBvP.js → notify-theme-1mOpY2vN.js} +1 -1
  85. package/src/client/dist/pwa/assets/{pending-diff-open-CVQxWyzP.js → pending-diff-open-BL7dKRqF.js} +1 -1
  86. package/src/client/dist/pwa/assets/{permissionModes-CVFHS2ec.js → permissionModes-BtC3_P-P.js} +1 -1
  87. package/src/client/dist/pwa/assets/{project-color-mcpQp2o6.js → project-color-BFBFZWeG.js} +1 -1
  88. package/src/client/dist/pwa/assets/{python-CO5iQN74.js → python-DoZAYim6.js} +1 -1
  89. package/src/client/dist/pwa/assets/{razor-BC7D8LMf.js → razor-9bZ30g2y.js} +1 -1
  90. package/src/client/dist/pwa/assets/{render-chat-markdown-Cb15kvox.js → render-chat-markdown-7EtWNjG0.js} +1 -1
  91. package/src/client/dist/pwa/assets/{typescript-BCHCSNpD.js → typescript-C1CBfL3L.js} +1 -1
  92. package/src/client/dist/pwa/assets/{use-tours-DhLhglwr.js → use-tours-BJ81clv6.js} +1 -1
  93. package/src/client/dist/pwa/assets/{workspace-templates-CoWrOEac.js → workspace-templates-r1-bipjM.js} +1 -1
  94. package/src/client/dist/pwa/assets/{xml-y9M3NAIZ.js → xml-COuu5isK.js} +1 -1
  95. package/src/client/dist/pwa/assets/{yaml-CthlToPo.js → yaml-ChZ7WqaJ.js} +1 -1
  96. package/src/client/dist/pwa/index.html +1 -1
  97. package/src/client/dist/pwa/sw.js +1 -1
  98. package/src/client/dist/pwa/assets/ActivityFeed-YRbIRutK.js +0 -8
  99. package/src/client/dist/pwa/assets/CreatePage-DxOyY65u.css +0 -1
  100. package/src/client/dist/pwa/assets/CreatePage-gFrn4xl9.js +0 -2
  101. package/src/client/dist/pwa/assets/WorkspacePage-B6O7j57u.js +0 -4
  102. package/src/client/dist/pwa/assets/i18n-DTfKQUMf.js +0 -1
package/AGENTS.md CHANGED
@@ -239,6 +239,8 @@ Each event maps to one effective-settings key (`sessionEndedScript` / `prMergedS
239
239
 
240
240
  ### Engine comparison
241
241
 
242
+ Successful paired creation opens the existing `split` route with `left` and `right` workspace ids in creation order. A single successful creation keeps the normal workspace route, including when the second creation fails.
243
+
242
244
  One task, two engines, two sibling worktrees. `POST /api/workspaces` accepts a `comparisonId`; the client posts twice with the same one (sequentially — parallel creations contend on the git index lock), suffixing the name with the engine's display name and the branch with the engine id and giving the non-configured engine the model picked for it on the form and its own effort / permission defaults. `workspaces.comparison_id` (migration v40) groups them; `listComparisonMembers` reads them back, treating an empty id as no group rather than as a match against the NULL majority. `GET /api/workspaces/:id/comparison` returns each member with its cached git stats (`null`, never zeros, when nothing has been measured). `ComparisonPanel.vue` renders the table at the top of the Git tab, only for workspaces that belong to a comparison. See [CONFIGURATION.md → Comparing two engines on one task](CONFIGURATION.md#comparing-two-engines-on-one-task).
243
245
 
244
246
  ### Unanswered question reminder
@@ -384,3 +386,16 @@ deviate from the documented system without explicit user approval.
384
386
  - Don't skip `try/catch` swallowing on best-effort cleanup (agent stop, dev-server stop, worktree removal). These must never break the primary operation.
385
387
  - Don't hardcode user-visible text in the frontend. Every string must go through `$t()` / `t()` with keys in all 5 locale files. See [Internationalization (i18n)](#internationalization-i18n).
386
388
  - Don't hardcode hex colors, spacing literals, or font names in components. Use the CSS variables from `src/client/src/css/design-tokens.scss` and the patterns in `DESIGN.md`. See [Design System](#design-system).
389
+
390
+
391
+ ## Audit remediation contracts (September 2026)
392
+
393
+ - `stopAgentAndWait` returns `stopped | not-running | timeout | failed`. Destructive callers must pass its result through `assertAgentStopped`. A timeout or failure retains the same controller in `stopping` until actual stop completion; it never authorizes a replacement or worktree removal. Pending replacements remain cancellable and are finalized as `killed` when stopped before starting. Dev-server shutdown retains ownership of the entire process group until no live member remains, including when the shell exits first; process inspection failures propagate to destructive callers. A successful custom stop can override a failed generic Docker shutdown only after an independent successful query confirms no running containers with the exact project label.
394
+ - Acquire `withWorkspaceLifecycleGuard` before `withGitRepoLock`. Agent/dev-server starts check lifecycle ownership centrally. Wakeups and cron defer while busy; auto-loop waiters are reconsidered on guard release and confirmed capacity release. A user stop persistently disables auto-loop, including while waiting for capacity without a controller. Internal stops explicitly pass `replacement`, `setup`, `archive` or `shutdown` to preserve that intent; delete and purge retain their dedicated causes.
395
+ - Quota-backoff `arm` receives the authoritative next `retryCount` from the orchestrator. Persist that value directly; consuming the preceding timer row must not reset the retry sequence.
396
+ - A quota retry with a residual controller awaits a confirmed technical stop before starting another iteration. Persist recovery before that await and retain its attempt count if the stop remains unconfirmed. Shutdown suspends quota, wakeup and cron timers without consuming their rows; late arms stay persisted but do not deliver until boot restores the schedulers. Automatic continuations also check shutdown when deferred lifecycle callbacks run, preserving auto-loop intent instead of treating a refused start as an error.
397
+ - Codex uses a 120-second stream-idle deadline and a bounded 30-minute deadline while normalized foreground tool calls remain unresolved. Synthetic `TodoWrite` calls emitted for completed plans must have matching results so they do not keep the tool deadline active. Human-input pauses remain independent of the tool timeout.
398
+ - Codex explicit stop confirms `exit` after each escalation, including `SIGKILL`; an unconfirmed exit rejects the stop instead of authorizing destructive follow-up. Exit listeners and deadlines are removed when that wait settles.
399
+ - Conversation loading indicators follow outstanding session/history/sync requests only while no content is available. Background refreshes must preserve the displayed conversation, component state and reading position, including feeds containing only user messages.
400
+ - PR diagnosis fingerprints are asynchronous and include binary staged/unstaged diffs and untracked file contents (symlink targets, not their destinations). Untracked nested Git repositories are fingerprinted recursively, including HEAD and index changes; ignored files and Git internals are excluded. Await them at every callsite. Workspace matching always includes normalized project identity.
401
+ - Daily DB backups are checked at startup and hourly, with the existing 24-hour minimum age and retention. Stop the scheduler and await its active backup before closing SQLite.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,15 @@ All notable changes to Kōbō are documented here. The format is based on
4
4
  [Keep a Changelog](https://keepachangelog.com/). Each release is an `## <version>`
5
5
  section — the in-app "What's new" dialog reads this file.
6
6
 
7
+ ## 1.11.37
8
+
9
+ - fix(kobo): harden workspace lifecycle and comparison navigation
10
+
11
+ ## 1.11.36
12
+
13
+ - fix(agent): resume quota waits and scheduled background checks
14
+ - ✨ feat(quota): Améliorer la détection et récupération des erreurs de limite de quota
15
+
7
16
  ## 1.11.35
8
17
 
9
18
  - feat(workspaces): add split view, activity digest and compact toolbar
@@ -29,12 +29,12 @@ import usageRoutes from './routes/usage.js';
29
29
  import voiceRouter from './routes/voice.js';
30
30
  import workspaceTemplatesRouter from './routes/workspace-templates.js';
31
31
  import workspacesRouter from './routes/workspaces.js';
32
- import { getAvailableSkills, isAgentUnavailableError, reconcileOrphanSessions, restoreRetryCountsFromDb, sendMessage, setBackendPort, startAgent, startWatchdog, stopAgent, stopAllAgents, stopWatchdog, } from './services/agent/orchestrator.js';
32
+ import { getAvailableSkills, isAgentUnavailableError, reconcileOrphanSessions, restoreRetryCountsFromDb, sendMessage, setBackendPort, startAgent, startWatchdog, stopAgentAndWait, stopAllAgents, stopWatchdog, } from './services/agent/orchestrator.js';
33
33
  import * as autoLoopService from './services/auto-loop-service.js';
34
34
  import { startAwaitingUserReminder, stopAwaitingUserReminder } from './services/awaiting-user-reminder-service.js';
35
35
  import { runContentMigrationIfNeeded } from './services/content-migration-service.js';
36
36
  import * as cronService from './services/cron-service.js';
37
- import { createDailyDbBackupIfNeeded, createPreMigrationBackup } from './services/db-backup-service.js';
37
+ import { createDailyDbBackupIfNeeded, createPreMigrationBackup, startDailyDbBackupScheduler, } from './services/db-backup-service.js';
38
38
  import { startDevServer, stopAllDevServers, stopDevServer } from './services/dev-server-service.js';
39
39
  import { authorizeWsUpgrade, generateToken, getLanHostnames, getLanUrls, isAllowedOrigin, isAllowedRequestHost, resolveBindHost, resolveDevClientOrigin, resolveNetworkAccessEnvOverrides, resolveProxyHostname, } from './services/network-access-service.js';
40
40
  import { startPrWatcher, stopPrWatcher } from './services/pr-watcher-service.js';
@@ -101,23 +101,11 @@ runRetentionPass('at boot');
101
101
  const RETENTION_INTERVAL_MS = 24 * 60 * 60 * 1000;
102
102
  const retentionTimer = setInterval(() => runRetentionPass('daily'), RETENTION_INTERVAL_MS);
103
103
  retentionTimer.unref?.();
104
- // Daily DB backup (best-effort, fire-and-forget — never blocks boot).
105
- // Creates a WAL-safe snapshot alongside kobo.db if no backup exists in the
106
- // last 24h, and rotates out older backups beyond the retention window.
107
- void createDailyDbBackupIfNeeded(db, getDbPath())
108
- .then((r) => {
109
- if (r.created) {
110
- startupDebug(`[kobo] Daily DB backup: ${r.created}`);
111
- if (r.deleted.length > 0) {
112
- startupDebug(`[kobo] Rotated ${r.deleted.length} old DB backup(s)`);
113
- }
114
- }
115
- })
116
- .catch((err) => {
117
- // A full disk or an unwritable backup directory must not stop the server
118
- // from booting: without this catch the rejection reaches the last-resort
119
- // handler below and Kōbō exits before it ever listens.
120
- console.error('[kobo] Daily DB backup failed (continuing):', err);
104
+ // Check on boot and hourly so servers running longer than a day keep backing up.
105
+ const dailyBackupScheduler = startDailyDbBackupScheduler(async () => {
106
+ const result = await createDailyDbBackupIfNeeded(db, getDbPath());
107
+ if (result.created)
108
+ startupDebug(`[kobo] Daily DB backup: ${result.created}`);
121
109
  });
122
110
  // Initialize process cleanup, agent watchdog, PR watcher, and wakeup rehydration
123
111
  reconcileOrphanSessions();
@@ -128,7 +116,7 @@ autoLoopService.rehydrate();
128
116
  // otherwise the next arm() after restart would compute the next ladder rung
129
117
  // from retryCount=0 and undo the progression.
130
118
  restoreRetryCountsFromDb();
131
- quotaBackoffService.restoreOnBoot((workspaceId) => autoLoopService.onQuotaBackoffExpired(workspaceId));
119
+ quotaBackoffService.restoreOnBoot((workspaceId, pending) => autoLoopService.onQuotaBackoffExpired(workspaceId, pending));
132
120
  cronService.restoreOnBoot();
133
121
  // Deliver any new default prompt templates to existing installs (seed-once via the
134
122
  // seededDefaultSlugs watermark; never overwrites or re-adds deleted defaults).
@@ -453,10 +441,8 @@ setMessageHandler(async (type, payload) => {
453
441
  }
454
442
  if (type === 'workspace:stop' && p?.workspaceId) {
455
443
  try {
456
- // A deliberate stop must not be treated like a recoverable engine
457
- // interruption: disable before the engine emits session:ended(killed).
458
- autoLoopService.disable(p.workspaceId, 'user-action');
459
- stopAgent(p.workspaceId);
444
+ // Share the HTTP stop contract, including loops waiting without an agent.
445
+ await stopAgentAndWait(p.workspaceId);
460
446
  }
461
447
  catch (err) {
462
448
  console.error('[ws] Failed to stop agent:', err);
@@ -613,6 +599,7 @@ async function gracefulShutdown(signal, exitCode = 0) {
613
599
  }
614
600
  finally {
615
601
  try {
602
+ await dailyBackupScheduler.stop();
616
603
  closeDb();
617
604
  console.log('[kobo] Database closed');
618
605
  }
@@ -3,6 +3,7 @@ import { migrationGuard } from '../middleware/migration-guard.js';
3
3
  import { getDevServerLogs, getStatus, startDevServer, stopDevServer } from '../services/dev-server-service.js';
4
4
  import { getProjectSettings } from '../services/settings-service.js';
5
5
  import { getWorkspace } from '../services/workspace-service.js';
6
+ import { WorkspaceLifecycleBusyError } from '../utils/workspace-lifecycle-guard.js';
6
7
  /** Hono sub-router for per-workspace dev server lifecycle (start, stop, status, logs). */
7
8
  const app = new Hono();
8
9
  // GET /api/dev-server/:workspaceId/status
@@ -51,7 +52,7 @@ app.post('/:workspaceId/start', migrationGuard, (c) => {
51
52
  }
52
53
  catch (err) {
53
54
  const message = err instanceof Error ? err.message : String(err);
54
- const status = message.includes('already starting') ? 409 : 500;
55
+ const status = err instanceof WorkspaceLifecycleBusyError || /already starting|is stopping/.test(message) ? 409 : 500;
55
56
  return c.json({ error: message }, status);
56
57
  }
57
58
  });
@@ -1,3 +1,4 @@
1
+ import path from 'node:path';
1
2
  import { Hono } from 'hono';
2
3
  import { getForgeProvider } from '../services/forge/registry.js';
3
4
  import { resolveForge } from '../services/forge/resolve.js';
@@ -61,7 +62,7 @@ app.post('/diagnose', async (c) => {
61
62
  ongoingOperation: null,
62
63
  branch: { state: 'absent' },
63
64
  };
64
- return c.json({ report, pr: null, fingerprint: prCheckout.computeFingerprint(report) });
65
+ return c.json({ report, pr: null, fingerprint: await prCheckout.computeFingerprint(report) });
65
66
  }
66
67
  // Page through the full list — a PR the user explicitly diagnosed by
67
68
  // number must never come back "not found" just for sitting past the
@@ -75,11 +76,11 @@ app.post('/diagnose', async (c) => {
75
76
  } while (!pr && cursor !== null);
76
77
  if (!pr)
77
78
  return c.json({ error: `Pull request #${prNumber} not found` }, 404);
78
- const workspaces = listWorkspaces(true);
79
+ const workspaces = listWorkspaces(true).filter((workspace) => path.resolve(workspace.projectPath) === path.resolve(projectPath));
79
80
  const report = prCheckout.diagnoseLocalState(projectPath, pr.headBranch, worktreesPath ?? null, attachedWorktreePaths(workspaces), workspaces);
80
81
  if (pr.isFork)
81
82
  report.blockers.push({ kind: 'fork-pr' });
82
- return c.json({ report, pr, fingerprint: prCheckout.computeFingerprint(report) });
83
+ return c.json({ report, pr, fingerprint: await prCheckout.computeFingerprint(report) });
83
84
  }
84
85
  catch (err) {
85
86
  return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
@@ -93,7 +94,7 @@ app.post('/resolve', async (c) => {
93
94
  if (!body.projectPath || !body.fingerprint) {
94
95
  return c.json({ error: 'Missing required fields: projectPath, fingerprint' }, 400);
95
96
  }
96
- const workspaces = listWorkspaces(true);
97
+ const workspaces = listWorkspaces(true).filter((workspace) => path.resolve(workspace.projectPath) === path.resolve(body.projectPath));
97
98
  const result = await prCheckout.resolvePrCheckout({
98
99
  projectPath: body.projectPath,
99
100
  headBranch: body.headBranch,