@loicngr/kobo 1.11.1 → 1.11.3

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 (88) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +6 -6
  3. package/dist/server/routes/workspaces.js +69 -1
  4. package/dist/server/services/agent/engines/claude-code/engine.js +1 -0
  5. package/dist/server/services/agent/engines/claude-code/event-mapper.js +1 -1
  6. package/dist/server/services/agent/engines/claude-code/options-builder.js +2 -0
  7. package/dist/server/services/agent/engines/codex/engine.js +1 -1
  8. package/dist/server/services/agent/engines/codex/event-mapper.js +1 -1
  9. package/dist/server/services/agent/orchestrator.js +91 -3
  10. package/dist/server/services/forge/bitbucket/provider.js +291 -0
  11. package/dist/server/services/forge/github/provider.js +10 -1
  12. package/dist/server/services/forge/gitlab/provider.js +10 -1
  13. package/dist/server/services/forge/none.js +10 -1
  14. package/dist/server/services/forge/registry.js +3 -1
  15. package/dist/server/services/forge/resolve.js +4 -1
  16. package/dist/server/services/settings-defaults.js +1 -1
  17. package/dist/server/services/settings-service.js +68 -12
  18. package/dist/shared/project-colors.js +7 -0
  19. package/package.json +1 -1
  20. package/src/client/dist/pwa/assets/ActivityFeed-DB_EucGW.js +8 -0
  21. package/src/client/dist/pwa/assets/{ChangelogPage-CppcIG5K.js → ChangelogPage-CNn9l8Ht.js} +1 -1
  22. package/src/client/dist/pwa/assets/{ClosePopup-B0-qi7Jo.js → ClosePopup-BycCSMYN.js} +1 -1
  23. package/src/client/dist/pwa/assets/CreatePage-B_JcmRfU.css +1 -0
  24. package/src/client/dist/pwa/assets/CreatePage-BnEu_-oB.js +2 -0
  25. package/src/client/dist/pwa/assets/{DiffViewer-COD5Mdp_.js → DiffViewer-DiV6QEdI.js} +3 -3
  26. package/src/client/dist/pwa/assets/{HealthPage-Bnad2BVm.js → HealthPage-ByBfOzgZ.js} +1 -1
  27. package/src/client/dist/pwa/assets/{MainLayout-Bwulksar.css → MainLayout-BY-xOjDK.css} +1 -1
  28. package/src/client/dist/pwa/assets/{MainLayout-nn4YIcy8.js → MainLayout-DOuqjNgy.js} +17 -17
  29. package/src/client/dist/pwa/assets/{QBtnToggle-CZXgD3Hf.js → QBtnToggle-BClaEu9N.js} +1 -1
  30. package/src/client/dist/pwa/assets/{QChip-BJH-iJCV.js → QChip-4nbXNOAl.js} +1 -1
  31. package/src/client/dist/pwa/assets/QSelect-BCz_9zhf.js +36 -0
  32. package/src/client/dist/pwa/assets/QTooltip-DSxTMMkV.js +1 -0
  33. package/src/client/dist/pwa/assets/{SearchPage-BwD6K8qD.js → SearchPage-BRv1M8JZ.js} +1 -1
  34. package/src/client/dist/pwa/assets/SettingsPage-BqsWfOOK.js +16 -0
  35. package/src/client/dist/pwa/assets/{SettingsPage-DJEH2ahl.css → SettingsPage-Os-6b-6H.css} +1 -1
  36. package/src/client/dist/pwa/assets/{ThinkingItem-DBWuhjgW.js → ThinkingItem-Bx_u0wMq.js} +1 -1
  37. package/src/client/dist/pwa/assets/{TouchPan-txugr_lm.js → TouchPan-a3VbuxQ6.js} +1 -1
  38. package/src/client/dist/pwa/assets/{WorkspacePage-Cv04uh_0.css → WorkspacePage-D-1HoeQg.css} +1 -1
  39. package/src/client/dist/pwa/assets/WorkspacePage-Dsj0f4XV.js +4 -0
  40. package/src/client/dist/pwa/assets/{build-path-tree-BUkEDxlT.js → build-path-tree-BKKu-ryI.js} +1 -1
  41. package/src/client/dist/pwa/assets/{cssMode-BDLIXvw9.js → cssMode-BKAdI7ff.js} +1 -1
  42. package/src/client/dist/pwa/assets/documents-Cc-OXiwm.js +1 -0
  43. package/src/client/dist/pwa/assets/{editor.api2-PhaZVUXg.js → editor.api2-BaCyAVGk.js} +1 -1
  44. package/src/client/dist/pwa/assets/{editor.main-2nNvSQc1.js → editor.main-BbVQG_bB.js} +2 -2
  45. package/src/client/dist/pwa/assets/{expand-template-HgUyAUBK.js → expand-template-CZOjiOqB.js} +1 -1
  46. package/src/client/dist/pwa/assets/{formatters-Chk4XGyr.js → formatters-CCEkVVIc.js} +1 -1
  47. package/src/client/dist/pwa/assets/{freemarker2-Dy02xxKp.js → freemarker2-Bh-TbpI2.js} +1 -1
  48. package/src/client/dist/pwa/assets/{handlebars-Do59Qec5.js → handlebars-B8nDlL6P.js} +1 -1
  49. package/src/client/dist/pwa/assets/{html-MJ9-LLjS.js → html-7xHGi5EV.js} +1 -1
  50. package/src/client/dist/pwa/assets/{htmlMode-AsZ-SMzZ.js → htmlMode-Bj3pQot4.js} +1 -1
  51. package/src/client/dist/pwa/assets/i18n-DXWy90AZ.js +1 -0
  52. package/src/client/dist/pwa/assets/index-CH7ykfX_.js +84 -0
  53. package/src/client/dist/pwa/assets/{javascript-DoZFpstd.js → javascript-BtBc5IVR.js} +1 -1
  54. package/src/client/dist/pwa/assets/{jsonMode-CMeFz2Y2.js → jsonMode-0IOmldeM.js} +1 -1
  55. package/src/client/dist/pwa/assets/{kobo-commands-DmJq1I6k.js → kobo-commands-2L1djzbe.js} +1 -1
  56. package/src/client/dist/pwa/assets/{layout-BSJkgTMQ.js → layout-DgfOrzwf.js} +1 -1
  57. package/src/client/dist/pwa/assets/{liquid-Bc70ZdlV.js → liquid-DF94cTT4.js} +1 -1
  58. package/src/client/dist/pwa/assets/{lspLanguageFeatures-Co4WnPYT.js → lspLanguageFeatures-iHqiZBzm.js} +1 -1
  59. package/src/client/dist/pwa/assets/{mdx-BsXN2I7z.js → mdx-C-x1eJF5.js} +1 -1
  60. package/src/client/dist/pwa/assets/{monaco.contribution-BpmQVvpQ.js → monaco.contribution-DSfodOAp.js} +2 -2
  61. package/src/client/dist/pwa/assets/{network-auth-B0RCBQ5K.js → network-auth-DA6dNTa5.js} +1 -1
  62. package/src/client/dist/pwa/assets/{pending-diff-open-BiozufWe.js → pending-diff-open-bDBsLjPx.js} +1 -1
  63. package/src/client/dist/pwa/assets/{permissionModes-kRWgzgvi.js → permissionModes-DQd7fbSy.js} +1 -1
  64. package/src/client/dist/pwa/assets/project-color-CQtsHSqi.js +1 -0
  65. package/src/client/dist/pwa/assets/{python-CV5Zt8gJ.js → python-fAiLpu-5.js} +1 -1
  66. package/src/client/dist/pwa/assets/{razor-4Ev2x3ig.js → razor-Clyexxhi.js} +1 -1
  67. package/src/client/dist/pwa/assets/{render-chat-markdown-Cjt11WnX.js → render-chat-markdown-MzPqUxrD.js} +1 -1
  68. package/src/client/dist/pwa/assets/{tsMode-BDysTsn1.js → tsMode-aOX259X0.js} +1 -1
  69. package/src/client/dist/pwa/assets/{typescript-DM1CefPN.js → typescript-BiPmygzZ.js} +1 -1
  70. package/src/client/dist/pwa/assets/{use-onboarding-D2BGKiP_.js → use-onboarding-NfnxjTzg.js} +1 -1
  71. package/src/client/dist/pwa/assets/{use-quasar-Cps11O9Y.js → use-quasar-CyeldpIw.js} +1 -1
  72. package/src/client/dist/pwa/assets/{workers-BuJZJ6j6.js → workers-BcOflh4r.js} +1 -1
  73. package/src/client/dist/pwa/assets/{xml-DzARVQ9o.js → xml-6qooibMC.js} +1 -1
  74. package/src/client/dist/pwa/assets/{yaml-P0ZYSZvj.js → yaml-DzDdyl50.js} +1 -1
  75. package/src/client/dist/pwa/index.html +1 -1
  76. package/src/client/dist/pwa/sounds/basic-notification.mp3 +0 -0
  77. package/src/client/dist/pwa/sw.js +1 -1
  78. package/src/client/dist/pwa/assets/ActivityFeed-ByJjMo0k.js +0 -8
  79. package/src/client/dist/pwa/assets/CreatePage-D0xvdJAQ.js +0 -2
  80. package/src/client/dist/pwa/assets/CreatePage-qMgdzdb-.css +0 -1
  81. package/src/client/dist/pwa/assets/QSelect-tsFTetLi.js +0 -36
  82. package/src/client/dist/pwa/assets/QTooltip-CA_Iyybc.js +0 -1
  83. package/src/client/dist/pwa/assets/SettingsPage-CF-xI5PO.js +0 -16
  84. package/src/client/dist/pwa/assets/WorkspacePage-DgucXHjd.js +0 -4
  85. package/src/client/dist/pwa/assets/documents-ClS4k_bT.js +0 -1
  86. package/src/client/dist/pwa/assets/i18n-Djv0PzK4.js +0 -1
  87. package/src/client/dist/pwa/assets/index-BpTtR9lB.js +0 -84
  88. package/src/client/dist/pwa/assets/project-color-D35ZMh9_.js +0 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,42 @@ 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.3
8
+
9
+ - feat(forge): add bitbucket community support
10
+
11
+ ## 1.11.2
12
+
13
+ - feat(integrations): let Notion and Sentry be enabled independently, keep
14
+ their settings isolated, hide inactive project overrides, and add detailed
15
+ **Test connection** feedback.
16
+ - feat(chat): stream Claude output token-by-token like Codex, consolidate agent
17
+ reasoning into one live panel above the composer, and add a visibility
18
+ preference for reasoning and verbose system messages.
19
+ - feat(chat): add `Ctrl+J` for a new line, `Ctrl+Enter` to force a queued
20
+ message while an agent is busy, and keep the shortcuts visible below chat.
21
+ - feat(chat): add session-aware `Ctrl+F` workspace-history search, scrolling to
22
+ the selected result; global search now opens the same exact message.
23
+ - feat(chat): add a `Ctrl+K` workspace command palette for common actions.
24
+ - feat(chat): make worktree-relative paths and Edit/Write cards open the
25
+ corresponding file in the Git diff viewer.
26
+ - feat(git): add workspace commits with either a user-written message or an
27
+ agent request; add confirmed PR/MR merging and an optional remote-branch
28
+ deletion after merge.
29
+ - feat(auto-loop): retry quota, rate-limit, and temporary upstream failures;
30
+ add a configurable maximum retry count.
31
+ - feat(notifications): add `basic-notification.mp3`, event-level sound
32
+ inheritance, an agent-error sound, browser-permission diagnostics, and more
33
+ reliable playback while Kobo is in a background tab.
34
+ - feat(settings): extend the project color palette and improve save-state
35
+ handling for activity-feed preferences.
36
+ - feat(observability): enrich the session timeline with tools, tokens, and
37
+ errors; keep it fresh during an active run and retain diagnostic export.
38
+ - feat(pwa): show install, offline, update, and WebSocket-reconnection states;
39
+ preserve the visible chat area when the status banner is displayed.
40
+ - fix(websocket): avoid reconnecting after an explicit disconnect and resume
41
+ safely after network connectivity returns.
42
+
7
43
  ## 1.11.1
8
44
 
9
45
  - feat(workspace): improve workspace actions
package/README.md CHANGED
@@ -17,11 +17,11 @@ Kōbō runs multiple coding agents in parallel, each isolated in its own git wor
17
17
 
18
18
  - **Isolated worktrees**: each workspace is a dedicated git worktree on its own branch, so parallel sessions never collide.
19
19
  - **Two agent engines**: Claude Code (via `@anthropic-ai/claude-agent-sdk`) and OpenAI Codex (via `codex app-server`), chosen per workspace.
20
- - **Live chat**: streaming text, inline Edit/Write diffs, per-turn cards, a compaction-in-progress indicator, infinite scrollback, and a reasoning panel immediately above the message input while Claude or Codex is actively thinking. `/` autocompletes skills and commands, `@` fuzzy-autocompletes worktree file paths, and `Ctrl+F` searches readable messages in the current workspace: selecting a result loads its session and scrolls to it. The global search page deep-links to the same location. Messages queued while an agent is busy stay scoped to their workspace and session. **Send now** delivers a queued message to an active Claude Code stream or steers the active Codex turn; it remains queued until the engine confirms acceptance. Use `Enter` to send, `Ctrl+Enter` to force delivery while an agent is busy, and `Shift+Enter` or `Ctrl+J` for a new line.
20
+ - **Live chat**: streaming text, inline Edit/Write diffs, per-turn cards, a compaction-in-progress indicator, infinite scrollback, and a reasoning panel immediately above the message input while Claude or Codex is actively thinking. `/` autocompletes skills and commands, `@` fuzzy-autocompletes worktree file paths, and `Ctrl+F` searches readable messages in the current workspace: selecting a result loads its session and scrolls to it. `Ctrl+K` opens a command palette for common workspace actions. The global search page deep-links to the same location. Messages queued while an agent is busy stay scoped to their workspace and session. **Send now** delivers a queued message to an active Claude Code stream or steers the active Codex turn; it remains queued until the engine confirms acceptance. Use `Enter` to send, `Ctrl+Enter` to force delivery while an agent is busy, and `Shift+Enter` or `Ctrl+J` for a new line.
21
21
  - **Full MCP toolset (`kobo-tasks`)**: a per-workspace MCP server the agent uses for far more than tasks — task/acceptance-criteria CRUD, starting/stopping the dev server and reading its logs, a unified `get_ticket` (Notion or Sentry), searching past conversations across every workspace, per-session token/cost usage, and a `.ai/thoughts` decision log. Native Claude Code Task tools complement it for lightweight sub-agent coordination. See [`AGENTS.md`](./AGENTS.md) for the full tool list.
22
22
 
23
23
  ![Sub-agents panel showing parallel tool calls](docs/assets/images/sub-agents-panel.png)
24
- - **Git panel**: a Monaco-based diff viewer with **inline file editing** (edit the right-hand panel directly, save with `Ctrl/Cmd+S`, conflict-guarded via sha precondition), inline conflict resolution, and `Sync` / `Push` / `Commit` / `Open PR` / `Merge ready PR` / `Change PR base` / `Change source branch`. You can write the commit message yourself or ask the active engine to prepare the commit; PR/MR merging always requires a confirmation. Edit/Write cards in chat open their exact changed file in the diff viewer. Multi-forge: GitHub (`gh`), GitLab (`glab`), or no forge, auto-detected from the remote and overridable per project.
24
+ - **Git panel**: a Monaco-based diff viewer with **inline file editing** (edit the right-hand panel directly, save with `Ctrl/Cmd+S`, conflict-guarded via sha precondition), inline conflict resolution, and `Sync` / `Push` / `Commit` / `Open PR` / `Merge ready PR` / `Change PR base` / `Change source branch`. You can write the commit message yourself or ask the active engine to prepare the commit; PR/MR merging always requires a confirmation, then Kōbō can offer to delete the merged remote branch. Edit/Write cards in chat open their exact changed file in the diff viewer. Multi-forge: GitHub (`gh`), GitLab (`glab`), Bitbucket Community (`bkt`), or no forge, auto-detected from the remote and overridable per project.
25
25
 
26
26
  ![Diff viewer with side-by-side changes](docs/assets/images/diff-viewer.png)
27
27
  - **Dev server panel**: start, stop, and tail logs for a workspace's dev server (Docker or npm) straight from the Tools panel — no need to leave the UI. When a project has no configured server, the panel and MCP tool report that explicitly instead of implying one is running.
@@ -29,15 +29,15 @@ Kōbō runs multiple coding agents in parallel, each isolated in its own git wor
29
29
  - **Interactive Q&A**: an agent can pause mid-session to ask a clarifying question through the UI; the workspace surfaces under "Needs Attention" until you answer. Choosing **Other** opens an inline free-form field, and option previews are available on hover. Codex native interactive questions require its `plan` permission mode.
30
30
 
31
31
  ![Agent asking a clarifying question, awaiting the user's answer](docs/assets/images/agent-question.png)
32
- - **Auto-loop**: an opt-in mode that walks the task list, spawning a fresh session per task and stopping once there's nothing left to do, progress stalls, an error occurs, or the agent needs input from you. Optionally run the initial brainstorming session on a stronger model and every task after that on a cheaper one, without leaving the engine you picked.
32
+ - **Auto-loop**: an opt-in mode that walks the task list, spawning a fresh session per task and stopping once there's nothing left to do, progress stalls, an error occurs, or the agent needs input from you. Rate-limit and temporary upstream failures retry on the quota backoff ladder; Settings controls the maximum number of automatic retries. Optionally run the initial brainstorming session on a stronger model and every task after that on a cheaper one, without leaving the engine you picked.
33
33
  - **Quota-aware**: 5-hour / 7-day Claude usage and Codex rate-limit buckets sit in the footer, and sessions auto-resume after a rate-limit reset.
34
34
  - **Scheduled wakeups**: the agent schedules a one-shot wake-up via the `schedule_wakeup` MCP tool. Kōbō persists it across restarts, shows a live countdown, and re-invokes the agent with the stored prompt at the chosen time.
35
35
  - **Cron schedules**: recurring per-workspace triggers the agent registers through MCP tools (`cron_create` / `cron_delete` / `cron_list`). Each tick resumes the workspace session (skipped if already active), and schedules are re-armed at boot with skip-missed semantics.
36
36
  - **Lifecycle scripts**: shell scripts run automatically at key moments — **setup** (worktree created), **cleanup** (session ended), **archive** (workspace archived). Configure them globally or per project, with their output streamed into the chat.
37
37
  - **Disk-space purge**: free a merged workspace's disk space without losing its chat history — see [below](#disk-space-purge).
38
- - **Observability**: a compact session timeline in the right panel shows session duration and status; download a redacted workspace diagnostic JSON for troubleshooting.
38
+ - **Observability**: a compact session timeline in the right panel shows each session’s duration, tools, tokens, and errors; download a redacted workspace diagnostic JSON for troubleshooting.
39
39
  - **Optional integrations**: independently enable or disable Notion (import missions) and Sentry (fix from issue URL) in Settings, with a **Test connection** action for each; local voice transcription uses whisper.cpp.
40
- - **Installable PWA**: production builds work as a Progressive Web App. Install Kōbō from your browser on desktop or mobile for an app-like window; it signals offline state and offers an explicit reload when an update is ready. Live agent activity still requires a connection to the local Kōbō server.
40
+ - **Installable PWA**: production builds work as a Progressive Web App. Install Kōbō from your browser on desktop or mobile for an app-like window; it signals offline state, WebSocket reconnection, and offers an explicit reload when an update is ready. Live agent activity still requires a connection to the local Kōbō server.
41
41
 
42
42
  ## Quick start
43
43
 
@@ -84,7 +84,7 @@ The most common knobs:
84
84
  | `ANTHROPIC_API_KEY` | none | Claude Code engine credential (alternative to `claude /login`) |
85
85
  | `OPENAI_API_KEY` | none | Codex engine credential (alternative to `codex login`) |
86
86
 
87
- Global and per-project settings (worktree path, dev server commands, E2E framework, prompt templates, git conventions, branch prefixes, lifecycle scripts, task prompt) are edited in **Settings** at runtime. Notion and Sentry are enabled independently in their respective tabs; disabling one keeps its configuration and existing workspace links, but removes its import and automation UI. In **Settings → General → Activity feed**, you can hide agent reasoning panels. Per-project values inherit from the global ones when left empty.
87
+ Global and per-project settings (worktree path, dev server commands, E2E framework, prompt templates, git conventions, branch prefixes, lifecycle scripts, task prompt) are edited in **Settings** at runtime. Notion and Sentry are enabled independently in their respective tabs; disabling one keeps its configuration and existing workspace links, but removes its import and automation UI. **Settings → Forge** holds local Bitbucket Community credentials for `bkt`; Kōbō uses them for PR actions and provides them only to agents working in Bitbucket projects. In **Settings → General → Activity feed**, you can hide agent reasoning panels. Per-project values inherit from the global ones when left empty.
88
88
 
89
89
  The full reference (every env var, every setting key, MCP server registration, Notion / Sentry / Voice setup) is in [`CONFIGURATION.md`](./CONFIGURATION.md).
90
90
 
@@ -1559,6 +1559,48 @@ function readableHistorySearchResult(type, rawPayload, query) {
1559
1559
  snippet: `${start > 0 ? '…' : ''}${text.slice(start, end).replace(/\s+/g, ' ').trim()}${end < text.length ? '…' : ''}`,
1560
1560
  };
1561
1561
  }
1562
+ // GET /api/workspaces/:id/session-metrics — compact observability data for the timeline.
1563
+ app.get('/:id/session-metrics', (c) => {
1564
+ try {
1565
+ const id = c.req.param('id');
1566
+ if (!workspaceService.getWorkspace(id))
1567
+ return c.json({ error: `Workspace '${id}' not found` }, 404);
1568
+ const db = getDb();
1569
+ const sessions = workspaceService.listSessions(id);
1570
+ const metrics = new Map(sessions.map((session) => [
1571
+ session.id,
1572
+ { sessionId: session.id, toolCalls: 0, errors: 0, inputTokens: 0, outputTokens: 0 },
1573
+ ]));
1574
+ const events = db
1575
+ .prepare("SELECT session_id, payload FROM ws_events WHERE workspace_id = ? AND type = 'agent:event' AND session_id IS NOT NULL")
1576
+ .all(id);
1577
+ for (const row of events) {
1578
+ const metric = metrics.get(row.session_id);
1579
+ if (!metric)
1580
+ continue;
1581
+ try {
1582
+ const event = JSON.parse(row.payload);
1583
+ if (event.kind === 'tool:call')
1584
+ metric.toolCalls++;
1585
+ if (event.kind === 'error' || (event.kind === 'tool:result' && event.isError === true))
1586
+ metric.errors++;
1587
+ if (event.kind === 'usage') {
1588
+ if (typeof event.inputTokens === 'number')
1589
+ metric.inputTokens = Math.max(metric.inputTokens, event.inputTokens);
1590
+ if (typeof event.outputTokens === 'number')
1591
+ metric.outputTokens = Math.max(metric.outputTokens, event.outputTokens);
1592
+ }
1593
+ }
1594
+ catch {
1595
+ // Corrupt historical events are intentionally ignored in diagnostics.
1596
+ }
1597
+ }
1598
+ return c.json({ sessions, metrics: [...metrics.values()] });
1599
+ }
1600
+ catch (err) {
1601
+ return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
1602
+ }
1603
+ });
1562
1604
  // GET /api/workspaces/:id/diagnostic.json — portable, redacted-by-design event diagnostic.
1563
1605
  app.get('/:id/diagnostic.json', (c) => {
1564
1606
  try {
@@ -3186,7 +3228,8 @@ app.post('/:id/merge-pr', async (c) => {
3186
3228
  return c.json({ error: 'This PR is not ready to merge', code: 'pr_not_ready' }, 409);
3187
3229
  }
3188
3230
  await provider.mergeRequest(workspace.worktreePath, snapshot.number);
3189
- return c.json({ success: true });
3231
+ const merged = await provider.getPrStatus(workspace.worktreePath, workspace.workingBranch);
3232
+ return c.json({ success: true, merged: merged?.state === 'MERGED', branch: workspace.workingBranch });
3190
3233
  }
3191
3234
  catch (err) {
3192
3235
  if (err instanceof ForgeUnavailableError) {
@@ -3196,6 +3239,31 @@ app.post('/:id/merge-pr', async (c) => {
3196
3239
  return c.json({ error: message }, 500);
3197
3240
  }
3198
3241
  });
3242
+ /** Delete the workspace branch from the remote after its PR/MR was merged. */
3243
+ app.post('/:id/delete-remote-branch', async (c) => {
3244
+ try {
3245
+ const id = c.req.param('id');
3246
+ const workspace = workspaceService.getWorkspace(id);
3247
+ if (!workspace)
3248
+ return c.json({ error: `Workspace '${id}' not found` }, 404);
3249
+ const provider = getForgeProvider(resolveForge(workspace.projectPath));
3250
+ if (!provider.capabilities.canDeleteRemoteBranch) {
3251
+ return c.json({ error: 'This project cannot delete remote branches', code: 'forge_unsupported' }, 409);
3252
+ }
3253
+ const availability = await provider.isAvailable(workspace.worktreePath);
3254
+ if (!availability.available)
3255
+ return c.json({ error: 'Forge CLI unavailable', code: 'forge_unavailable' }, 409);
3256
+ const snapshot = await provider.getPrStatus(workspace.worktreePath, workspace.workingBranch);
3257
+ if (snapshot?.state !== 'MERGED')
3258
+ return c.json({ error: 'The PR is not merged yet', code: 'pr_not_merged' }, 409);
3259
+ await provider.deleteRemoteBranch(workspace.worktreePath, workspace.workingBranch);
3260
+ return c.json({ success: true });
3261
+ }
3262
+ catch (err) {
3263
+ const message = err instanceof Error ? err.message : String(err);
3264
+ return c.json({ error: message }, 500);
3265
+ }
3266
+ });
3199
3267
  /** Re-target a workspace onto a new source branch (metadata + worktree + PR base). */
3200
3268
  app.post('/:id/change-source-branch', async (c) => {
3201
3269
  const id = c.req.param('id');
@@ -199,6 +199,7 @@ export function createClaudeCodeEngine() {
199
199
  console.warn(`[claude-engine stderr] ${data}`);
200
200
  }
201
201
  },
202
+ env: options.env,
202
203
  });
203
204
  sdkOptions.abortController = abortController;
204
205
  // Override the SDK's libc-blind binary resolution on Linux glibc — see
@@ -76,7 +76,7 @@ function normalizeRateLimitInfo(info) {
76
76
  * Anthropic's surfaces (`rate_limit_exceeded`, `Claude AI usage limit
77
77
  * reached`, `You're out of extra usage`, `quota exceeded`).
78
78
  */
79
- export const QUOTA_PATTERN = /out of extra usage|rate[_ ]limit|usage limit|quota exceeded/i;
79
+ export const QUOTA_PATTERN = /\b429\b|out of extra usage|rate[_ ]limit|usage limit|quota exceeded/i;
80
80
  export function createMapperState() {
81
81
  return {
82
82
  sessionStartedEmitted: false,
@@ -82,5 +82,7 @@ export function buildClaudeOptions(input) {
82
82
  options.canUseTool = input.canUseTool;
83
83
  if (input.stderr)
84
84
  options.stderr = input.stderr;
85
+ if (input.env)
86
+ options.env = input.env;
85
87
  return { options, effectivePrompt: prompt };
86
88
  }
@@ -60,7 +60,7 @@ export function createCodexEngine() {
60
60
  };
61
61
  const streamingBatcher = createStreamingBatcher(emitDirect);
62
62
  const safeEmit = (ev) => streamingBatcher.push(ev);
63
- const child = spawnAppServer({ cwd: options.workingDir, signal: abortController.signal });
63
+ const child = spawnAppServer({ cwd: options.workingDir, env: options.env, signal: abortController.signal });
64
64
  if (child.stderr) {
65
65
  child.stderr.setEncoding('utf8');
66
66
  child.stderr.on('data', (chunk) => {
@@ -7,7 +7,7 @@ import { nanoid } from 'nanoid';
7
7
  // - "quota exceeded" / "insufficient_quota" / "insufficient quota"
8
8
  // - "out of extra usage" / "out of usage"
9
9
  // - "usage limit exceeded"
10
- export const QUOTA_PATTERN = /rate[_ ]limit|quota|usage limit|insufficient[_ ]quota|out of (extra )?usage/i;
10
+ export const QUOTA_PATTERN = /\b429\b|rate[_ ]limit|quota|usage limit|insufficient[_ ]quota|out of (extra )?usage/i;
11
11
  export function createMapperState(opts) {
12
12
  return {
13
13
  sessionStartedEmitted: false,
@@ -6,8 +6,9 @@ import { unregisterProcess } from '../../utils/process-tracker.js';
6
6
  import * as autoLoopService from '../auto-loop-service.js';
7
7
  import * as cleanupScriptService from '../cleanup-script-service.js';
8
8
  import * as cronService from '../cron-service.js';
9
+ import { resolveForge } from '../forge/resolve.js';
9
10
  import * as quotaBackoffService from '../quota-backoff-service.js';
10
- import { getEffectiveSettings } from '../settings-service.js';
11
+ import { getEffectiveSettings, getGlobalSettings } from '../settings-service.js';
11
12
  import { refreshNow } from '../usage/poller.js';
12
13
  import * as wakeupService from '../wakeup-service.js';
13
14
  import { emit, emitEphemeral } from '../websocket-service.js';
@@ -342,6 +343,38 @@ function buildMcpServers(workspaceId) {
342
343
  },
343
344
  ];
344
345
  }
346
+ function isBitbucketProject(projectPath) {
347
+ try {
348
+ return resolveForge(projectPath) === 'bitbucket-community';
349
+ }
350
+ catch {
351
+ return false;
352
+ }
353
+ }
354
+ function buildAgentEnv(projectPath) {
355
+ if (!isBitbucketProject(projectPath))
356
+ return undefined;
357
+ const global = getGlobalSettings();
358
+ if (!global.bitbucketToken)
359
+ return undefined;
360
+ return {
361
+ ...process.env,
362
+ BKT_HOST: 'https://bitbucket.org',
363
+ BKT_TOKEN: global.bitbucketToken,
364
+ BKT_USERNAME: global.bitbucketUsername,
365
+ };
366
+ }
367
+ function buildAgentPrompt(prompt, projectPath) {
368
+ if (!projectPath || !isBitbucketProject(projectPath))
369
+ return prompt;
370
+ return [
371
+ '[Kōbō Bitbucket] Bitbucket credentials are available through the `bkt` CLI in this session.',
372
+ 'Use `bkt pr edit <number> --body <description>` and `bkt pr comment <number> --body <comment>` for PR updates.',
373
+ 'Do not call the Bitbucket REST API with curl or reuse BKT_TOKEN as a Bearer token: Bitbucket Cloud API tokens require the bkt CLI authentication flow.',
374
+ '',
375
+ prompt,
376
+ ].join('\n');
377
+ }
345
378
  function resolveSessionForResume(workspaceId, existingSessionId, model) {
346
379
  const db = getDb();
347
380
  let lastSession;
@@ -540,6 +573,12 @@ function handleEvent(workspaceId, agentSessionId, ev) {
540
573
  if (ev.kind === 'error' && ev.category === 'quota') {
541
574
  void handleQuota(workspaceId, agentSessionId);
542
575
  }
576
+ if (ev.kind === 'error' &&
577
+ ev.category === 'other' &&
578
+ TRANSIENT_SERVER_ERROR_PATTERN.test(ev.message) &&
579
+ getWs(workspaceId)?.autoLoop) {
580
+ void handleTransientAutoLoopFailure(workspaceId);
581
+ }
543
582
  if (ev.kind === 'error' && ev.category === 'resume_failed') {
544
583
  resumeFailedSet.add(workspaceId);
545
584
  clearStaleEngineSessionId(workspaceId);
@@ -755,7 +794,7 @@ export function startAgent(workspaceId, workingDir, prompt, model, resume = fals
755
794
  const options = {
756
795
  workspaceId,
757
796
  workingDir,
758
- prompt,
797
+ prompt: buildAgentPrompt(prompt, ws?.projectPath),
759
798
  model,
760
799
  effort: reasoningEffort,
761
800
  // Cascade: explicit caller override → workspace setting → 'bypass'.
@@ -772,6 +811,7 @@ export function startAgent(workspaceId, workingDir, prompt, model, resume = fals
772
811
  })(),
773
812
  settings,
774
813
  mcpServers: buildMcpServers(workspaceId),
814
+ env: ws ? buildAgentEnv(ws.projectPath) : undefined,
775
815
  };
776
816
  const controller = new SessionController(workspaceId, agentSessionId, engine, (ev) => handleEvent(workspaceId, agentSessionId, ev));
777
817
  controllers.set(workspaceId, controller);
@@ -1195,6 +1235,8 @@ function clearStaleEngineSessionId(workspaceId) {
1195
1235
  console.error('[orchestrator] Failed to clear stale engine session ID:', err);
1196
1236
  }
1197
1237
  }
1238
+ /** Retryable upstream failures for an auto-loop iteration (not coding errors). */
1239
+ export const TRANSIENT_SERVER_ERROR_PATTERN = /\b(?:http\s*)?500\b|internal server error|service unavailable|temporarily unavailable|overloaded/i;
1198
1240
  const QUOTA_SAFETY_MARGIN_MS = 30_000;
1199
1241
  const QUOTA_MAX_BACKOFF_MS = 24 * 60 * 60 * 1000;
1200
1242
  const QUOTA_SATURATION_THRESHOLD_PCT = 95;
@@ -1214,7 +1256,11 @@ const QUOTA_FALLBACK_LADDER_MINUTES = [15, 30, 60, 180, 300];
1214
1256
  * finally (2) a fixed ladder (15 → 30 → 60 → 180 → 300 min) whenever neither
1215
1257
  * source is usable.
1216
1258
  */
1217
- export async function computeQuotaBackoffMs(workspaceId, retryCount) {
1259
+ export async function computeQuotaBackoffMs(workspaceId, retryCount, preferQuotaReset = true) {
1260
+ if (!preferQuotaReset) {
1261
+ const idx = Math.min(Math.max(0, retryCount), QUOTA_FALLBACK_LADDER_MINUTES.length - 1);
1262
+ return { delayMs: QUOTA_FALLBACK_LADDER_MINUTES[idx] * 60 * 1000, source: 'fallback_ladder' };
1263
+ }
1218
1264
  // 1. Prefer the rate_limit event from the agent stream — most recent + most precise.
1219
1265
  const info = latestRateLimitInfo.get(workspaceId);
1220
1266
  if (info?.buckets?.length) {
@@ -1270,14 +1316,56 @@ async function handleQuota(workspaceId, _agentSessionId) {
1270
1316
  // May fail if transition is not valid
1271
1317
  }
1272
1318
  const retryCount = retryCounts.get(workspaceId) ?? 0;
1319
+ const autoLoopEnabled = getWs(workspaceId)?.autoLoop === true;
1320
+ const maxRetries = autoLoopEnabled ? (getGlobalSettings().autoLoopMaxRetries ?? 5) : 5;
1321
+ if (autoLoopEnabled && retryCount >= maxRetries) {
1322
+ autoLoopService.disable(workspaceId, 'error');
1323
+ try {
1324
+ updateWorkspaceStatus(workspaceId, 'error');
1325
+ }
1326
+ catch {
1327
+ // The loop is disabled even if an already-terminal status rejects this transition.
1328
+ }
1329
+ return;
1330
+ }
1273
1331
  const { delayMs, resetsAt, source } = await computeQuotaBackoffMs(workspaceId, retryCount);
1274
1332
  retryCounts.set(workspaceId, retryCount + 1);
1275
1333
  // The quotaBackoffService owns the timer + the persistent row + the
1276
1334
  // 'agent:quota-backoff' WS emit. Hand off everything to it.
1277
1335
  quotaBackoffService.arm(workspaceId, delayMs, { resetsAt: resetsAt ?? null, source });
1278
1336
  }
1337
+ /**
1338
+ * Use the same persisted retry path for temporary upstream failures (HTTP 500
1339
+ * and overloads) while deliberately ignoring quota-reset data: a server error
1340
+ * is unrelated to the user's quota window and follows the fallback ladder.
1341
+ */
1342
+ async function handleTransientAutoLoopFailure(workspaceId) {
1343
+ try {
1344
+ updateWorkspaceStatus(workspaceId, 'quota');
1345
+ }
1346
+ catch {
1347
+ // May fail if transition is not valid.
1348
+ }
1349
+ const retryCount = retryCounts.get(workspaceId) ?? 0;
1350
+ const maxRetries = getGlobalSettings().autoLoopMaxRetries ?? 5;
1351
+ if (retryCount >= maxRetries) {
1352
+ autoLoopService.disable(workspaceId, 'error');
1353
+ try {
1354
+ updateWorkspaceStatus(workspaceId, 'error');
1355
+ }
1356
+ catch {
1357
+ // The loop is disabled even if an already-terminal status rejects this transition.
1358
+ }
1359
+ return;
1360
+ }
1361
+ const { delayMs, resetsAt, source } = await computeQuotaBackoffMs(workspaceId, retryCount, false);
1362
+ retryCounts.set(workspaceId, retryCount + 1);
1363
+ quotaBackoffService.arm(workspaceId, delayMs, { resetsAt: resetsAt ?? null, source });
1364
+ }
1279
1365
  /** @internal test-only — re-export of `handleQuota` for direct testing. */
1280
1366
  export const _handleQuota = handleQuota;
1367
+ /** @internal test-only — re-export of the transient auto-loop retry handler. */
1368
+ export const _handleTransientAutoLoopFailure = handleTransientAutoLoopFailure;
1281
1369
  /**
1282
1370
  * Rebuild the in-memory `retryCounts` map from the persisted `pending_quota_backoffs`
1283
1371
  * rows. Called from `index.ts` at boot, before `quotaBackoffService.restoreOnBoot`.