@loicngr/kobo 1.11.39 → 1.11.41

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 (154) hide show
  1. package/AGENTS.md +101 -32
  2. package/CHANGELOG.md +8 -0
  3. package/README.md +6 -4
  4. package/dist/mcp-server/kobo-tasks-handlers.js +16 -9
  5. package/dist/mcp-server/kobo-tasks-server.js +13 -2
  6. package/dist/mcp-server/workspace-dialogue-client.js +28 -0
  7. package/dist/server/db/mcp-message-schema.js +16 -0
  8. package/dist/server/db/migrations.js +4 -0
  9. package/dist/server/db/schema.js +4 -0
  10. package/dist/server/db/search-schema.js +53 -0
  11. package/dist/server/index.js +17 -86
  12. package/dist/server/middleware/network-auth-middleware.js +5 -1
  13. package/dist/server/routes/changelog.js +4 -49
  14. package/dist/server/routes/git.js +2 -2
  15. package/dist/server/routes/images.js +65 -5
  16. package/dist/server/routes/mcp.js +47 -0
  17. package/dist/server/routes/pull-requests.js +1 -1
  18. package/dist/server/routes/search.js +13 -4
  19. package/dist/server/routes/settings.js +5 -0
  20. package/dist/server/routes/workspaces.js +250 -248
  21. package/dist/server/services/agent/engines/claude-code/engine.js +31 -33
  22. package/dist/server/services/agent/engines/codex/engine.js +37 -8
  23. package/dist/server/services/agent/engines/codex/event-mapper.js +3 -1
  24. package/dist/server/services/agent/engines/codex/options-builder.js +4 -3
  25. package/dist/server/services/agent/engines/codex/server-requests.js +8 -1
  26. package/dist/server/services/agent/orchestrator.js +20 -3
  27. package/dist/server/services/attachment-service.js +122 -0
  28. package/dist/server/services/change-source-branch-service.js +10 -42
  29. package/dist/server/services/chat-receipt-service.js +33 -0
  30. package/dist/server/services/cron-service.js +27 -1
  31. package/dist/server/services/image-service.js +16 -7
  32. package/dist/server/services/mcp-client-context.js +12 -0
  33. package/dist/server/services/mcp-connection-info-service.js +63 -0
  34. package/dist/server/services/mcp-message-delivery-service.js +59 -0
  35. package/dist/server/services/mcp-message-request-service.js +64 -0
  36. package/dist/server/services/pr-checkout-service.js +10 -10
  37. package/dist/server/services/pr-watcher-service.js +14 -9
  38. package/dist/server/services/search/indexer.js +232 -0
  39. package/dist/server/services/search/worker.js +76 -0
  40. package/dist/server/services/search-service.js +104 -110
  41. package/dist/server/services/update-check-service.js +119 -0
  42. package/dist/server/services/websocket-service.js +121 -35
  43. package/dist/server/services/workspace-dialogue-service.js +128 -0
  44. package/dist/server/services/workspace-message-service.js +58 -0
  45. package/dist/server/services/workspace-permission-policy-service.js +33 -3
  46. package/dist/server/services/worktree-restore-service.js +2 -2
  47. package/dist/server/services/worktree-service.js +197 -124
  48. package/dist/server/utils/bounded-process.js +137 -0
  49. package/dist/server/utils/git-ops.js +76 -128
  50. package/dist/server/utils/git-repo-lock.js +27 -13
  51. package/dist/shared/attachments.js +58 -0
  52. package/dist/shared/workspace-dialogue-tools.js +89 -0
  53. package/dist/shared/workspace-message-types.js +19 -0
  54. package/package.json +2 -1
  55. package/src/client/dist/pwa/assets/{ActivityFeed-Chsw1Mh7.css → ActivityFeed-BYR5x7aa.css} +1 -1
  56. package/src/client/dist/pwa/assets/ActivityFeed-DTIe6O9o.js +8 -0
  57. package/src/client/dist/pwa/assets/{ChangelogPage-D8563008.js → ChangelogPage-BWeu0YeI.js} +1 -1
  58. package/src/client/dist/pwa/assets/{ComparisonPanel-Bd0X08sg.js → ComparisonPanel-o_EHlTwV.js} +2 -2
  59. package/src/client/dist/pwa/assets/CreatePage-CGi88k6z.css +1 -0
  60. package/src/client/dist/pwa/assets/CreatePage-DbY5gBv8.js +2 -0
  61. package/src/client/dist/pwa/assets/DashboardPage-CIo_gesD.js +1 -0
  62. package/src/client/dist/pwa/assets/{DiffViewer-DnR79ycb.css → DiffViewer-DEPOa3D7.css} +1 -1
  63. package/src/client/dist/pwa/assets/DiffViewer-DEcf48KI.js +8 -0
  64. package/src/client/dist/pwa/assets/{DocumentsPanel-BJsJ0fNs.js → DocumentsPanel-D9vcB91W.js} +1 -1
  65. package/src/client/dist/pwa/assets/{DrawerToggleButton-BVLO5K5d.js → DrawerToggleButton-Ca0_CxSl.js} +1 -1
  66. package/src/client/dist/pwa/assets/GitPanel-DCmB2R5i.js +3 -0
  67. package/src/client/dist/pwa/assets/{HealthPage-RFZS7Lao.js → HealthPage-_q3vy_xn.js} +1 -1
  68. package/src/client/dist/pwa/assets/MainLayout-Duwnq3cc.js +2 -0
  69. package/src/client/dist/pwa/assets/{QBtnDropdown-X8oNU5Mb.js → QBtnDropdown-BeiZIHVA.js} +1 -1
  70. package/src/client/dist/pwa/assets/{QDrawer-Cij17skX.js → QDrawer-Fj3M9QGD.js} +1 -1
  71. package/src/client/dist/pwa/assets/QSelect-DtSUuK8t.js +1 -0
  72. package/src/client/dist/pwa/assets/{QTooltip-CwcCGSWq.js → QTooltip-DwkX8eQ6.js} +1 -1
  73. package/src/client/dist/pwa/assets/SchedulePanel-BAA4Aajn.css +1 -0
  74. package/src/client/dist/pwa/assets/SchedulePanel-BlZDd7TK.js +1 -0
  75. package/src/client/dist/pwa/assets/SearchPage-BGc1oQSM.css +1 -0
  76. package/src/client/dist/pwa/assets/SearchPage-BGi1yRxU.js +1 -0
  77. package/src/client/dist/pwa/assets/SettingsPage-DnO-ImWE.js +9 -0
  78. package/src/client/dist/pwa/assets/SettingsPage-sVlsJQ0b.css +1 -0
  79. package/src/client/dist/pwa/assets/{SplitWorkspacePage-D7lGFRKs.js → SplitWorkspacePage-vz6jN1DM.js} +1 -1
  80. package/src/client/dist/pwa/assets/{TerminalPanel-DI8DHb9e.js → TerminalPanel-IdPIGp2_.js} +9 -9
  81. package/src/client/dist/pwa/assets/{TouchPan-B3nrkCY8.js → TouchPan-DTJqxVp9.js} +1 -1
  82. package/src/client/dist/pwa/assets/{TourReplayButton-DU1UShMY.js → TourReplayButton-Bj2GGyhU.js} +1 -1
  83. package/src/client/dist/pwa/assets/{WorkspaceAttentionLabels-C-yN5IX8.js → WorkspaceAttentionLabels-BBaGZlwv.js} +1 -1
  84. package/src/client/dist/pwa/assets/WorkspacePage-DCg9zSxz.css +1 -0
  85. package/src/client/dist/pwa/assets/WorkspacePage-Dy0spsfi.js +4 -0
  86. package/src/client/dist/pwa/assets/{browser-f9MHQjfi.js → browser-BFj5wwbS.js} +1 -1
  87. package/src/client/dist/pwa/assets/{build-path-tree-BWBzP8os.js → build-path-tree-C-HSiySk.js} +1 -1
  88. package/src/client/dist/pwa/assets/de-BdLe7apJ.js +17 -0
  89. package/src/client/dist/pwa/assets/{documents-C2cLnEBQ.js → documents-C2Xnjh9Z.js} +1 -1
  90. package/src/client/dist/pwa/assets/{editor.api-sw146MWN.js → editor.api-BbKfnNmv.js} +1 -1
  91. package/src/client/dist/pwa/assets/{efforts-C5ZZgCH4.js → efforts-BM7fdaaL.js} +1 -1
  92. package/src/client/dist/pwa/assets/engineFeatures-b74YRODT.js +1 -0
  93. package/src/client/dist/pwa/assets/es-C1LM-3aq.js +17 -0
  94. package/src/client/dist/pwa/assets/expand-template-DiDWiIkx.js +1 -0
  95. package/src/client/dist/pwa/assets/{format-BwWP6Aay.js → format-CnbUxAmw.js} +1 -1
  96. package/src/client/dist/pwa/assets/{formatters-DyWtO-nR.js → formatters-B2uUIbAY.js} +1 -1
  97. package/src/client/dist/pwa/assets/fr-KCK6r5ga.js +17 -0
  98. package/src/client/dist/pwa/assets/{freemarker2-DZ5Hm8UH.js → freemarker2-Cu1vGY0x.js} +1 -1
  99. package/src/client/dist/pwa/assets/{handlebars-BRUGpTA7.js → handlebars-CETuqLyz.js} +1 -1
  100. package/src/client/dist/pwa/assets/{html-DNOayNNs.js → html-BTNpr39P.js} +1 -1
  101. package/src/client/dist/pwa/assets/i18n-BkUirOzl.js +1 -0
  102. package/src/client/dist/pwa/assets/index-mvh5w1cM.js +20 -0
  103. package/src/client/dist/pwa/assets/it-BM6t0IGU.js +17 -0
  104. package/src/client/dist/pwa/assets/{javascript-C0tDQU3z.js → javascript-C7Ncb6V7.js} +1 -1
  105. package/src/client/dist/pwa/assets/{liquid-CN46O6OF.js → liquid-D0hUdg4x.js} +1 -1
  106. package/src/client/dist/pwa/assets/{mdx-DM-lrr8Q.js → mdx-BK5H1Xst.js} +1 -1
  107. package/src/client/dist/pwa/assets/{monaco-contributions-CkN7y-8c.js → monaco-contributions-BBW6JNgz.js} +1 -1
  108. package/src/client/dist/pwa/assets/{monaco.contribution-2BcAOLjc.js → monaco.contribution-1LZrAlY4.js} +2 -2
  109. package/src/client/dist/pwa/assets/network-auth-CVGS__6a.js +1 -0
  110. package/src/client/dist/pwa/assets/{notify-theme-a91Tcr6o.js → notify-theme-DIKLxr8F.js} +1 -1
  111. package/src/client/dist/pwa/assets/{pending-diff-open-DOtELZfB.js → pending-diff-open-B02zuH1j.js} +1 -1
  112. package/src/client/dist/pwa/assets/{permissionModes-C-9Crj-c.js → permissionModes-C3otN1Vk.js} +1 -1
  113. package/src/client/dist/pwa/assets/{project-color-BRe4oyNx.js → project-color-BUcW_eZA.js} +1 -1
  114. package/src/client/dist/pwa/assets/{python-BRpJG6sZ.js → python-DEeUVvnp.js} +1 -1
  115. package/src/client/dist/pwa/assets/{razor-CehCmvHl.js → razor-CG6cL8ya.js} +1 -1
  116. package/src/client/dist/pwa/assets/{render-chat-markdown-DlbQLZ02.js → render-chat-markdown-Ct_T5TPN.js} +1 -1
  117. package/src/client/dist/pwa/assets/{typescript-Dt_8mVuj.js → typescript-CSQAg5UC.js} +1 -1
  118. package/src/client/dist/pwa/assets/use-search-index-status-DtLzygcy.js +1 -0
  119. package/src/client/dist/pwa/assets/use-tours-_6HwJ_Rp.js +2 -0
  120. package/src/client/dist/pwa/assets/{workspace-templates-GCObcjO6.js → workspace-templates-B8WWsWEI.js} +1 -1
  121. package/src/client/dist/pwa/assets/{xml-_gOwdzB4.js → xml-BSB4-DXR.js} +1 -1
  122. package/src/client/dist/pwa/assets/{yaml-BxeBqpGf.js → yaml-CSGKph85.js} +1 -1
  123. package/src/client/dist/pwa/index.html +1 -1
  124. package/src/client/dist/pwa/sw.js +1 -1
  125. package/src/mcp-server/README.md +169 -9
  126. package/src/mcp-server/kobo-tasks-handlers.ts +14 -11
  127. package/src/mcp-server/kobo-tasks-server.ts +19 -2
  128. package/src/mcp-server/workspace-dialogue-client.ts +34 -0
  129. package/src/client/dist/pwa/assets/ActivityFeed-BzzvcOmq.js +0 -8
  130. package/src/client/dist/pwa/assets/CreatePage-A3pVradx.css +0 -1
  131. package/src/client/dist/pwa/assets/CreatePage-Bon8qIka.js +0 -2
  132. package/src/client/dist/pwa/assets/DashboardPage-DYxdxEul.js +0 -1
  133. package/src/client/dist/pwa/assets/DiffViewer-BpsLNxnj.js +0 -8
  134. package/src/client/dist/pwa/assets/GitPanel-Cal_wzC0.js +0 -3
  135. package/src/client/dist/pwa/assets/MainLayout-vvyiRcql.js +0 -2
  136. package/src/client/dist/pwa/assets/QSelect-xd-AlGo_.js +0 -1
  137. package/src/client/dist/pwa/assets/SchedulePanel-BunlzsOY.css +0 -1
  138. package/src/client/dist/pwa/assets/SchedulePanel-CXRUsz6G.js +0 -1
  139. package/src/client/dist/pwa/assets/SearchPage-CVgRa025.css +0 -1
  140. package/src/client/dist/pwa/assets/SearchPage-DopbgLxh.js +0 -1
  141. package/src/client/dist/pwa/assets/SettingsPage-Do9d0Pov.css +0 -1
  142. package/src/client/dist/pwa/assets/SettingsPage-Jx6TnOzR.js +0 -9
  143. package/src/client/dist/pwa/assets/WorkspacePage-BAC6u9OP.css +0 -1
  144. package/src/client/dist/pwa/assets/WorkspacePage-DKRpdpPg.js +0 -4
  145. package/src/client/dist/pwa/assets/de-QkDLrcGq.js +0 -17
  146. package/src/client/dist/pwa/assets/engineFeatures-CAVMDawq.js +0 -1
  147. package/src/client/dist/pwa/assets/es-BgQFKlo8.js +0 -17
  148. package/src/client/dist/pwa/assets/expand-template-B6xAlAaM.js +0 -1
  149. package/src/client/dist/pwa/assets/fr-DAdLWjNM.js +0 -17
  150. package/src/client/dist/pwa/assets/i18n-CBez_vip.js +0 -1
  151. package/src/client/dist/pwa/assets/index-VL3fI9a3.js +0 -20
  152. package/src/client/dist/pwa/assets/it-Bumaewp1.js +0 -17
  153. package/src/client/dist/pwa/assets/network-auth-KYS4snQH.js +0 -1
  154. package/src/client/dist/pwa/assets/use-tours-CSVM_54K.js +0 -2
package/AGENTS.md CHANGED
@@ -4,9 +4,9 @@ Guidance for AI coding agents (Claude Code, Cursor, etc.) working on this reposi
4
4
 
5
5
  ## What this project is
6
6
 
7
- **Kōbō** (工房, Japanese for "workshop") orchestrates multiple Claude Code agents across isolated git worktrees. Each "workspace" is a self-contained mission with its own worktree, branch, Claude session, optional dev server, optional Notion source-of-truth, and a dedicated MCP tools server. A Vue 3 UI lets the human track progress, read live agent output, and manage the lifecycle.
7
+ **Kōbō** (工房, Japanese for "workshop") orchestrates Claude Code and OpenAI Codex agents across isolated git worktrees. Each "workspace" is a self-contained mission with its own worktree, branch, agent sessions, optional dev server, optional Notion/Sentry context, and a dedicated MCP tools server. A Vue 3 UI lets the human track progress, read live agent output, review Git changes, and manage the lifecycle. A workspace can switch engines through a handoff to a fresh session while preserving its worktree and history.
8
8
 
9
- Single-user dev tool, local by default. The server binds `127.0.0.1` unless the opt-in "network access" setting is enabled, which binds all interfaces and gates non-loopback HTTP/WS requests behind a shared token (localhost always exempt). No multi-tenant concerns.
9
+ Single-user dev tool, local by default. The server binds `127.0.0.1` unless the opt-in "network access" setting is enabled, which binds all interfaces and gates non-loopback API/WS requests behind a shared token. Behind-a-reverse-proxy mode also requires the token for loopback API/WS requests; `/api/health` stays exempt. Host/Origin checks apply separately. No multi-tenant concerns.
10
10
 
11
11
  ## Tech stack
12
12
 
@@ -16,9 +16,9 @@ Single-user dev tool, local by default. The server binds `127.0.0.1` unless the
16
16
 
17
17
  **Database**: a single SQLite file under the **Kōbō home directory** (`~/.config/kobo/kobo.db` by default, overridable via `KOBO_HOME`). Fresh-install schema lives in `src/server/db/schema.ts` (`initSchema`); incremental migrations live in `src/server/db/migrations.ts`. **The project is in production**, so every schema change MUST ship as a migration that preserves data, never as a breaking change to `initSchema` alone. See [Database migrations](#database-migrations) below.
18
18
 
19
- **Kōbō home directory**: `KOBO_HOME` env var overrides everything. Otherwise `$XDG_CONFIG_HOME/kobo/`, else `~/.config/kobo/`. Contains `kobo.db`, `settings.json`, `skills.json`, `templates.json`. **Development uses `./data/`** via the `KOBO_HOME=./data` prefix in the `dev` npm script, so local dev never touches your real `~/.config/kobo/` and can run in parallel with a production-installed Kōbō (`npx @loicngr/kobo`). See `src/server/utils/paths.ts`.
19
+ **Kōbō home directory**: `KOBO_HOME` env var overrides everything. Otherwise `$XDG_CONFIG_HOME/kobo/`, else `~/.config/kobo/`. Contains `kobo.db`, `settings.json`, `skills.json`, `templates.json`, and `workspace-templates.json`. **Development uses `./data/`** via the `KOBO_HOME=./data` prefix in the `dev` npm script, so local dev never touches your real `~/.config/kobo/` and can run in parallel with a production-installed Kōbō (`npx @loicngr/kobo`). See `src/server/utils/paths.ts`.
20
20
 
21
- **Tests**: Vitest covers backend services/routes and frontend stores/pure utilities. Vue components are not unit-tested; type-checking and manual smoke tests cover their behavior. Run `npm test` and `(cd src/client && npm test)` for the complete suites.
21
+ **Tests**: Vitest covers backend services/routes and frontend stores, utilities, composables, and selected Vue components. The client suite uses Vue Test Utils and `happy-dom`; type-checking and manual smoke tests complement it. `npm test` runs the backend suite only; `npm run test:all` runs both suites. The three dependency trees (root, client, PWA) have separate lockfiles.
22
22
 
23
23
  ## Commands
24
24
 
@@ -36,8 +36,12 @@ npm run dev:all # both concurrently
36
36
  # Check & test
37
37
  npm run lint # biome check (linting + formatting)
38
38
  npx tsc --noEmit # type check backend (the project's primary quality gate)
39
- npm test # run full vitest suite (backend + client tests)
40
- (cd src/client && npm test) # client tests only (stores, utils)
39
+ npm run typecheck:tests # type check backend test fixtures and tests
40
+ (cd src/client && npm run type-check) # vue-tsc: client + Vue components
41
+ npm test # backend vitest suite
42
+ (cd src/client && npm test) # client stores, utilities, composables and components
43
+ npm run test:all # backend + client suites
44
+ make ci # install, audit, lint, type checks, build, both suites
41
45
  npm run test:watch # vitest in watch mode
42
46
 
43
47
  # Build & run
@@ -56,15 +60,15 @@ src/
56
60
  │ ├── db/
57
61
  │ │ ├── index.ts # singleton getDb / closeDb
58
62
  │ │ ├── schema.ts # initSchema — CREATE TABLE IF NOT EXISTS …
59
- │ │ └── migrations.ts # incremental migrations, bumped per feature
63
+ │ │ └── migrations.ts # append-only migration registry + transactional runner
60
64
  │ ├── services/ # business logic — pure functions over db + external processes
61
65
  │ │ ├── workspace-service.ts # workspaces + tasks + agent_sessions CRUD
62
66
  │ │ ├── agent/ # agent engine abstraction (replaces former agent-manager.ts)
63
67
  │ │ │ ├── orchestrator.ts # per-workspace engine map, retry/quota handling, watchdog, public API
64
68
  │ │ │ ├── session-controller.ts # lifecycle wrapper around an AgentEngine instance
65
- │ │ │ ├── event-router.ts # maps engine AgentEvent stream to WS emit + DB side-effects
69
+ │ │ │ ├── event-router.ts # persists/broadcasts normalized AgentEvent via WebSocket
66
70
  │ │ │ └── engines/{claude-code,codex}/ # per-engine adapters implementing the AgentEngine contract
67
- │ │ ├── forge/ # ForgeProvider abstraction — github / gitlab / none, with registry + auto-resolve
71
+ │ │ ├── forge/ # github / gitlab / bitbucket-community / none, registry + auto-resolve
68
72
  │ │ ├── change-source-branch-service.ts # re-target a workspace onto a new source branch (built-in cherry-pick or custom bash)
69
73
  │ │ ├── git-stats-service.ts # pure compute of commit/ahead-behind/diff stats + forge availability for a workspace
70
74
  │ │ ├── settings-defaults.ts # DEFAULT_* constants for opt-in settings (e.g. change-source-branch script)
@@ -83,7 +87,8 @@ src/
83
87
  │ │ ├── workspaces.ts # /api/workspaces/* — the main surface
84
88
  │ │ ├── templates.ts # /api/templates — prompt templates CRUD
85
89
  │ │ ├── workspace-templates.ts # /api/workspace-templates - saved create-form presets
86
- │ │ ├── plans.ts # /api/workspaces/:id/plans — plan file browser (read-only)
90
+ │ │ ├── documents.ts # workspace plans, specs and thoughts browser (read-only)
91
+ │ │ ├── mcp.ts # /api/mcp — external discovery and conversation tools
87
92
  │ │ ├── dev-server.ts, git.ts, notion.ts, settings.ts
88
93
  │ ├── utils/
89
94
  │ │ ├── git-ops.ts # pushBranch, pullBranch, getCommitsBetween, delete{Local,Remote}Branch…
@@ -91,7 +96,7 @@ src/
91
96
  ├── client/ # Vue 3 + Quasar PWA
92
97
  │ └── src/
93
98
  │ ├── stores/ # pinia: workspace, websocket, settings, dev-server, templates
94
- │ ├── components/ # WorkspaceList, NotionPanel, AcceptancePanel, ChatInput, GitPanel, PlansPanel, WorkspaceAttentionLabels, HelpMenu…
99
+ │ ├── components/ # WorkspaceList, AcceptancePanel, ChatInput, GitPanel, DocumentsPanel, WorkspaceAttentionLabels, HelpMenu…
95
100
  │ ├── utils/ # expand-template, formatters, workspace-attention (CI failure / changes-requested derivation)…
96
101
  │ ├── tours/ # declarative onboarding tours, one file per zone + registry
97
102
  │ ├── pages/ # WorkspacePage, CreatePage, SettingsPage
@@ -99,7 +104,8 @@ src/
99
104
  ├── mcp-server/ # standalone MCP server spawned per workspace
100
105
  │ ├── kobo-tasks-server.ts # entrypoint, registers tools
101
106
  │ └── kobo-tasks-handlers.ts # pure handlers (list_tasks, mark_task_done)
102
- └── __tests__/ # vitest, one file per service/route
107
+ ├── shared/ # model catalogues, prompts, dialogue contracts, shared utilities
108
+ └── __tests__/ # backend Vitest suites; client tests live under client/src/__tests__/
103
109
  ```
104
110
 
105
111
  ## Data model (SQLite)
@@ -110,7 +116,14 @@ src/
110
116
  | `tasks` | workspace sub-items: title, status, `is_acceptance_criterion`, sort_order; CASCADE DELETE on workspace |
111
117
  | `agent_sessions` | agent-engine sessions: pid where applicable, engine session id, status, timestamps, model, and name |
112
118
  | `ws_events` | persisted WebSocket events for replay on reconnect: type, payload, session_id, created_at |
113
- | `pending_wakeups` | one-row-per-workspace scheduler for the `ScheduleWakeup` tool: target_at (ISO UTC), prompt, reason; CASCADE DELETE on workspace |
119
+ | `pending_wakeups` | one-row-per-workspace scheduler for the `schedule_wakeup` MCP tool: target_at (ISO UTC), prompt, reason; CASCADE DELETE on workspace |
120
+ | `pending_quota_backoffs` | persisted quota retries and their attempt counts |
121
+ | `pending_crons` | recurring workspace schedules and their next run |
122
+ | `session_event_metrics` / `usage_snapshots` | session event counters and provider usage snapshots |
123
+ | `workspace_activity` | significant lifecycle/PR events for the absence digest |
124
+ | `search_*` | derived logical-message search index and indexing progress; see `search-schema.ts` |
125
+ | `mcp_message_requests` | durable external-message idempotency ledger; independent of event retention |
126
+ | `schema_migrations` | applied migration versions, names and timestamps |
114
127
  | `workspace_chat_history` | chat-input history per workspace: message text + `created_at`, ordered by autoincrement id, capped at 200 entries by the service; CASCADE DELETE on workspace |
115
128
  | `workspace_permission_rules` | remembered per-workspace tool approvals, scoped to an exact operation or every invocation of a tool; CASCADE DELETE on workspace |
116
129
 
@@ -132,8 +145,8 @@ src/
132
145
 
133
146
  ### The two files and their roles
134
147
 
135
- - **`src/server/db/schema.ts`**: `initSchema(db)` is the source of truth for **fresh installs only**. It creates every table at its current shape. New installations (empty `data/` directory) run `initSchema` once and land at the latest `SCHEMA_VERSION`.
136
- - **`src/server/db/migrations.ts`**: `runMigrations(db)` reads the current `version` from the `schema_version` table and sequentially applies every pending migration block up to `SCHEMA_VERSION`. Existing databases upgrade through this path.
148
+ - **`src/server/db/schema.ts`**: `initSchema(db)` creates the current application schema for **fresh installs only**, including the search and MCP-message schemas through their helpers.
149
+ - **`src/server/db/migrations.ts`**: the ordered `migrations` registry contains `{ version, name, migrate(db) }` entries. `SCHEMA_VERSION` is derived from its last entry. `runMigrations(db)` records applied entries in `schema_migrations(version, name, applied_at)` and applies each pending entry in its own transaction. A fresh installation runs `initSchema` and records all entries as applied in one transaction. Legacy `schema_version` databases are converted to the history table automatically.
137
150
 
138
151
  Both files must be kept in sync: after adding a migration, update `initSchema` so fresh installs get the same final shape without replaying migrations.
139
152
 
@@ -141,10 +154,10 @@ Both files must be kept in sync: after adding a migration, update `initSchema` s
141
154
 
142
155
  Every feature that touches the schema:
143
156
 
144
- 1. Bump `SCHEMA_VERSION` in `migrations.ts` (e.g. `1` → `2`)
145
- 2. Add a new guarded block at the bottom of `runMigrations` that applies only if `currentVersion < newVersion`, using `db.exec` (better-sqlite3) to run raw SQL like `ALTER TABLE workspaces ADD COLUMN new_field TEXT`
157
+ 1. Append an entry to `migrations` with the next version and a descriptive name. `SCHEMA_VERSION` updates automatically; do not replace its derived expression with a literal.
158
+ 2. Implement the entry's `migrate(db)` using raw SQL, such as `ALTER TABLE workspaces ADD COLUMN new_field TEXT`. The runner owns the transaction and skips versions already recorded.
146
159
  3. Update `initSchema` in `schema.ts` so the fresh-install shape matches (e.g. add the new column to the `CREATE TABLE` statement)
147
- 4. At the end of `runMigrations`, bump the row in `schema_version` to the new `SCHEMA_VERSION` (the existing code already does this)
160
+ 4. Let the runner record the new entry in `schema_migrations`; do not manually update a legacy `schema_version` row.
148
161
  5. Add a test in `src/__tests__/migrations.test.ts` that verifies:
149
162
  - A database at the previous version can be upgraded without data loss
150
163
  - The new version matches `SCHEMA_VERSION`
@@ -157,18 +170,31 @@ Every feature that touches the schema:
157
170
  - **Always idempotent where possible.** Use `IF NOT EXISTS`, check for column existence before altering, etc. Prefer migrations that can be safely re-run.
158
171
  - **`ALTER TABLE ADD COLUMN` is safe in SQLite** (even on large tables). For more invasive changes (rename, drop, change type), use the [12-step SQLite pattern](https://sqlite.org/lang_altertable.html#otheralter) within a transaction.
159
172
 
160
- - **Run migrations on every backend start.** `runMigrations(db)` is called from `getDb()` via `src/server/db/index.ts`.
173
+ - **Run migrations on every backend start.** `src/server/index.ts` opens the connection with `getDb()`, checks for pending migrations, attempts a pre-migration backup, then calls `runMigrations(db)`. `getDb()` only opens/configures SQLite; it does not initialize or migrate the schema. Tests creating their own database must initialize it explicitly.
174
+ - **Settings migrations are separate.** The JSON settings migration versions in `settings-service.ts` are independent of SQLite's `SCHEMA_VERSION`.
161
175
  - **Test upgrades, not just fresh installs.** The `migrations.test.ts` suite must exercise "old DB → new DB" paths.
162
176
 
163
177
  ## WebSocket protocol
164
178
 
179
+ ### External MCP dialogue and update checks
180
+
181
+ `POST /api/mcp` exposes stateless Streamable HTTP discovery/dialogue tools, behind the existing Host/Origin and network-token gates. The global stdio server forwards dialogue calls through this backend so agent runtime ownership remains centralized. `workspace-message-service.ts` shares delivery with WebSocket chat.
182
+
183
+ Optional `send_workspace_message.idempotency_key` uses `mcp_message_requests` (migration v43). Reserve before dispatch, preserve fingerprints and terminal outcomes, and never automatically resend an unfinished request after restart. The user event and accepted receipt commit in one SQLite transaction; broadcast follows commit. Dispatch may precede persistence, so interrupted requests are explicitly `delivery_unknown`, not an exactly-once execution promise. The backend alone calls `reconcileMessageRequests` on startup. History retention does not delete the ledger; workspace deletion cascades it.
184
+
185
+ MCP messages and question answers include optional `source: { kind: 'mcp', clientName, transport }`; keep it across live, sync, history and conversation grouping paths without changing legacy `sender` behavior. Client names are display labels, not authorization identities. Unicode HTTP labels use URI encoding with `X-Kobo-Client-Name-Encoding: uri`; stdio forwards the initialization name or `KOBO_MCP_CLIENT_NAME` override.
186
+
187
+ `update-check-service.ts` owns the single immediate/every-20-minute npm release check. It is explicitly started/stopped by the backend, coalesces route/poller calls, and broadcasts ephemeral global `kobo:update-checked` events. The client update store also refreshes at reconnect. Do not reintroduce a per-tab registry poll or automatically install updates.
188
+
189
+ The explicit `npm run test:mcp:live` / `make test-mcp-live` command requires `KOBO_LIVE_ENGINE` and `KOBO_LIVE_MODEL`, uses real credentials and disposable data, and must stay outside `make ci`. Normal CI uses deterministic transport/service tests.
190
+
165
191
  Clients subscribe to individual workspace ids. The server sends `WsEvent` objects:
166
192
 
167
193
  ```ts
168
194
  { id, workspaceId, type, payload, sessionId?, createdAt }
169
195
  ```
170
196
 
171
- Common types: `agent:output`, `agent:status`, `agent:error`, `user:message`, `task:updated`, `devserver:status`, `workspace:archived`, `workspace:unarchived`, `sync:response`.
197
+ Agent engines emit a normalized `AgentEvent` union, carried by `agent:event`. Common outer types include `user:message`, `task:updated`, `devserver:status`, `workspace:status`, `workspace:archived`, `workspace:unarchived`, and `sync:response`. Legacy `agent:output` rows remain supported through content migration. Keep the backend union in `services/agent/engines/types.ts` and its client mirror in `client/src/types/agent-event.ts` synchronized.
172
198
 
173
199
  Two emit flavors in `websocket-service.ts`:
174
200
  - `emit(workspaceId, type, payload)` persists to `ws_events` for later replay via `sync:request` on reconnect
@@ -178,7 +204,7 @@ Two emit flavors in `websocket-service.ts`:
178
204
 
179
205
  ### Forge providers
180
206
 
181
- `src/server/services/forge/` implements a `ForgeProvider` interface with three concrete providers: `github` (wraps the `gh` CLI), `gitlab` (wraps the `glab` CLI), and `none` (a no-op that disables PR/MR features cleanly). The public surface is two functions: `getForgeProvider(name)` in `registry.ts` (returns the provider for a named forge) and `resolveForge(projectPath)` in `resolve.ts` (reads the per-project `forge` setting, then falls back to auto-detection from the `origin` remote URL: host contains `github.com` → GitHub, host contains `gitlab` → GitLab, otherwise `none`). The per-project `forge` setting (`'auto' | 'github' | 'gitlab' | 'none'`, default `'auto'`) is stored in `settings.json` and seeded by settings migration v32. PR routes (`open-pr`, `change-pr-base`) and the pr-watcher go through the resolved provider. **Kōbō ships no forge credentials**, so the user must install and authenticate `gh` or `glab` themselves; when the CLI is absent or unauthenticated, PR/MR actions are disabled with a tooltip rather than a raw error.
207
+ `src/server/services/forge/` implements `ForgeProvider` for `github` (`gh`), `gitlab` (`glab`), `bitbucket-community` (`bkt`), and `none` (disables PR/MR features). `getForgeProvider(id)` resolves a provider; `resolveForge(projectPath)` first reads the explicit project setting, then classifies the `origin` URL by `github.com`, `gitlab`, or `bitbucket`, falling back to `none`. Successful remote reads are cached for five minutes. PR routes and the watcher use the resolved provider. **Kōbō ships no forge credentials**: users authenticate `gh`/`glab` themselves; Bitbucket uses the email and API token entered in Settings, passed to `bkt` and excluded from settings exports. Missing or unauthenticated CLIs disable PR/MR actions with an explanation.
182
208
 
183
209
  ### Notion (opt-in, user-provided credentials)
184
210
 
@@ -188,7 +214,7 @@ Two emit flavors in `websocket-service.ts`:
188
214
  2. `NOTION_TOKEN` env var
189
215
  3. `~/.claude.json` → `mcpServers.notion.env.NOTION_TOKEN` / `NOTION_API_TOKEN` (Claude Code's MCP config: the recommended path, the same token shared with Claude Code)
190
216
 
191
- The MCP command and args can be overridden via `NOTION_MCP_COMMAND` (default `npx`) and `NOTION_MCP_ARGS` (default `-y @notionhq/notion-mcp-server`) for pinning a specific version or using a fork.
217
+ The selected Claude config entry defaults to the enabled key named exactly `notion`; `global.notionMcpKey` selects another exact key. Its command/args are reused unless `NOTION_MCP_COMMAND` / `NOTION_MCP_ARGS` override them; otherwise the fallback is `npx -y @notionhq/notion-mcp-server`. Existing `OPENAPI_MCP_HEADERS` are preserved; when absent, the resolved token is used to construct them.
192
218
 
193
219
  When adding features touching `notion-service.ts`, remember: **no token = no feature**. The rest of Kōbō must keep working if the Notion token is absent; only the explicit Notion import endpoints should fail with a clear error. Do not throw at server startup.
194
220
 
@@ -198,7 +224,7 @@ See the "Notion integration" section of the README for the end-user setup guide.
198
224
 
199
225
  Two engines live under `src/server/services/agent/engines/`, both implementing the `AgentEngine` contract in `types.ts`:
200
226
 
201
- **Claude Code** (`claude-code/`): uses `@anthropic-ai/claude-agent-sdk` (in-process async iterator). Spawns no subprocess. Auth via `~/.claude.json` or `ANTHROPIC_API_KEY` env var. The engine arms a **15 s result-drain watchdog** when the SDK emits its `result` message: if the async iterator does not close cleanly within the window, `session:ended` is force-emitted so the orchestrator and auto-loop never hang on a stuck generator. The watchdog is idempotent via a `sessionEndedEmitted` guard and the timer is cleared in `finally`.
227
+ **Claude Code** (`claude-code/`): consumes the `@anthropic-ai/claude-agent-sdk` async iterator. The SDK manages the Claude runtime; Kōbō does not launch a separate `claude` CLI command. Authentication reuses the user's Claude login or `ANTHROPIC_API_KEY`. The engine arms a **15 s result-drain watchdog** when the SDK emits its `result` message: if the async iterator does not close cleanly within the window, `session:ended` is force-emitted so the orchestrator and auto-loop never hang on a stuck generator. The watchdog is idempotent via a `sessionEndedEmitted` guard and the timer is cleared in `finally`.
202
228
 
203
229
  **OpenAI Codex** (`codex/`): uses the **`codex app-server` JSON-RPC protocol** (line-delimited JSON over stdio with a long-lived `codex` subprocess). The engine layers are:
204
230
  - `jsonrpc/transport.ts` + `jsonrpc/peer.ts`: generic JSON-RPC 2.0 stdio peer (request correlation, notifications, server-initiated requests)
@@ -220,17 +246,60 @@ Background: the engine was migrated from `@openai/codex-sdk` (one-shot `codex ex
220
246
  - **Permission mode vs collaboration mode are independent.** Sandbox + approvalPolicy control *what the agent may do at OS level* (read-only / workspace-write, never / on-request / unless-trusted). `collaborationMode` is a separate session-level flag that gates internal Codex behaviour (notably interactive Q&A). Kōbō hides both behind a single "permission mode" selector and maps them together.
221
247
  - **Sub-agents map to `collabAgentToolCall`.** Codex's analogue of Claude's Task tool is `collabAgentToolCall` (`spawnAgent` / `sendInput` / `resumeAgent` / `wait` / `closeAgent`). The mapper emits **both** a `tool:call` named `Task` (chat card) and a `subagent:progress` event (right-hand panel) per call, the same dual-emission Claude does. See `event-mapper.ts` `handleItemStarted` / `handleItemCompleted` for the `collabAgentToolCall` branch.
222
248
  - **`fileChange` items carry a unified-diff blob.** The protocol shape is `{ path, kind: PatchChangeKind, diff: string }` per change; `kind` is a discriminated union, not a string. The mapper flattens the first change into a Claude-style Edit input (`{ file_path, diff, change_kind, move_path? }`) so the existing `ToolCallItem` renderer picks it up. The client parses the unified diff into `DiffLine[]` via `parseUnifiedDiff` in `inline-diff.ts`.
223
- - **Streaming bursts trip auto-scroll.** Codex emits one `message:text` event per token-delta (50-200 per message), versus Claude which emits ~1 per content block. The naive `eventCount` watcher in `ActivityFeed.vue` triggered an animated `scrollToBottom(180)` per event, causing stacked animations and visible jank. The fix coalesces requests through `requestAnimationFrame` and only animates the *first* scroll after a quiet period; subsequent scrolls during a burst snap instantly.
249
+ - **Streaming bursts trip auto-scroll.** Both engines can emit token deltas; Claude enables SDK partial messages as well. The naive `eventCount` watcher in `ActivityFeed.vue` triggered an animated `scrollToBottom(180)` per event, causing stacked animations and visible jank. Scroll requests are coalesced through `requestAnimationFrame`; preserve burst handling when changing the feed.
224
250
  - **`MCP tools` need `default_tools_approval_mode: 'auto'` in `config.mcp_servers`.** Without it Codex flags every MCP tool call as needing user approval ("user cancelled MCP tool call"). Kōbō trusts every tool it spawns, so the options-builder pre-approves the namespace.
225
251
 
226
252
  ## Workspace operations
227
253
 
254
+ ### Attachments during creation
255
+
256
+ `CreationAttachments.vue` keeps `File[]` in the create form, with image-only
257
+ object-URL previews revoked on removal/unmount. The workspace store sends
258
+ existing JSON requests unchanged when there are no files; attachments use
259
+ multipart fields `workspace` (one JSON string) and repeated `attachments`.
260
+ The legacy `images` field is accepted too; both fields share validation limits.
261
+ `attachment-service.ts` validates before workspace/git side effects,
262
+ then saves after the worktree exists and before setup/agent launch. Images use
263
+ `image-service.ts`; documents use generated filenames in `.ai/attachments/`,
264
+ exclusive writes and the existing directory-containment helper. File ownership
265
+ is recorded immediately after exclusive open so partial writes are cleaned on
266
+ failure. Creation excludes `.ai/attachments/` from Git; document uploads also
267
+ ensure that exclusion for existing workspaces. Never use an
268
+ uploaded filename as a storage path. The initial prompt stores image references
269
+ and document names/paths for normal starts and setup retries. Documents are
270
+ copied byte-for-byte, with no PDF extraction or OCR.
271
+ Limits and supported formats live in `src/shared/attachments.ts`:
272
+ 10 files, 50 MiB each, 50 MiB total; the creation route limits the body to 51 MiB.
273
+ Document validation uses supported filename extensions because browser MIME
274
+ labels can be empty or generic. A failed creation cleans its own uploaded files
275
+ from reused worktrees, preserving existing files; setup failures retain the
276
+ workspace, files and initial prompt. Comparison requests copy the same browser
277
+ files independently. Attachments are not persisted in form presets.
278
+ See [creation attachments](CONFIGURATION.md#attachments-in-the-creation-description).
279
+
280
+ ### Chat attachments
281
+
282
+ `POST /api/workspaces/:id/attachments` in `routes/images.ts` accepts one multipart
283
+ `attachment`, validates it with the shared `attachments.ts` policy and returns
284
+ `uid`, `kind`, `path`, `originalName`, and `reference`. It uses the same storage as
285
+ creation and a lifecycle guard against concurrent purge/delete. Document deletion
286
+ accepts only the generated filename at `DELETE /:id/attachments/:filename`; images
287
+ retain their existing retrieval/deletion endpoints. The legacy image upload also
288
+ uses the 50 MiB limit.
289
+
290
+ `use-chat-attachments.ts` owns pending upload state, draft-level limits and serial
291
+ uploads. `ChatInput.vue` transfers file ownership when sending/queuing, restores
292
+ badges on failed sends, and discards unsent files on workspace/session changes.
293
+ Late upload receipts are cleaned up using the captured original workspace id.
294
+ Never delete handed-off files when clearing a draft or changing conversations.
295
+ See [chat attachments](CONFIGURATION.md#attachments-in-workspace-chat).
296
+
228
297
  ### Change source branch
229
298
 
230
299
  `change-source-branch-service.ts` re-targets a workspace onto a new source branch. The default path is a cherry-pick of the branch-proper commits (commits in the working branch but in **neither** the old nor the new base), inspired by the sekur `deploy-preprod-rebase.yml` workflow. The route is `POST /api/workspaces/:id/change-source-branch` and returns a discriminated status: `done | aligned | conflict | too-many | dirty`.
231
300
 
232
301
  - **Built-in cherry-pick**: `fetchAllBranches` → `listProperCommits` → `stashPush` (if dirty + aligned) → backup branch (`kobo-backup/<branch>-<unix-ts>`) → `reset --hard origin/<new>` → cherry-pick replay → optional force-push prompt → forge PR-base update via `provider.changePrBase`. Conflicts leave the worktree in a cherry-pick state for the user/agent to resolve via `POST /:id/git/resolve-with-agent`. `GitConflictError` carries an `operation: 'rebase' | 'merge' | 'cherry-pick'` discriminator.
233
- - **Custom bash override**: if `effective.changeSourceBranchScript` is non-empty (per-project override or global default), the script **replaces** the built-in flow. Spawned with `bash -c`, cwd = worktree, 5 min timeout, stderr captured (last 8 KB). Exit 0 → Kōbō updates the source-branch metadata; any non-zero exit → the stderr tail is propagated as a clean error. The user-facing menu item only shows when the resolved script is non-empty; empty means the feature is disabled (opt-in).
302
+ - **Custom bash override**: if `effective.changeSourceBranchScript` is non-empty (per-project override or global default), the script **replaces** the built-in flow. Spawned with `bash -c`, cwd = worktree, 5 min timeout, stderr captured (last 8 KB). Exit 0 → Kōbō updates the source-branch metadata; any non-zero exit → the stderr tail is propagated as a clean error. The user-facing menu item only shows when the resolved script is non-empty; clearing the effective script hides the action; the global default is the bundled Kōbō script.
234
303
  - **Custom-script env vars**: `KOBO_NEW_BASE`, `KOBO_OLD_BASE`, `KOBO_WORKING_BRANCH`, `KOBO_WORKTREE_PATH`, `KOBO_PROJECT_PATH`, `KOBO_PROJECT_NAME`, `KOBO_WORKSPACE_ID`, `KOBO_WORKSPACE_NAME`, `KOBO_FORGE`, `KOBO_PR_NUMBER` (empty when no PR/MR is open). The default script lives in `settings-defaults.ts` and is seeded into `global.changeSourceBranchScript` by settings migration v33; the client reads it through `GET /api/settings/defaults` for the "Reset to Kōbō default" button. See [CONFIGURATION.md → Custom change-source-branch script](CONFIGURATION.md#custom-change-source-branch-script).
235
304
 
236
305
  ### Lifecycle hooks
@@ -263,9 +332,9 @@ One task, two engines, two sibling worktrees. `POST /api/workspaces` accepts a `
263
332
 
264
333
  `src/server/services/worktree-purge-service.ts` removes a workspace's worktree from disk while preserving the chat history and PR metadata. Triggered manually via `POST /api/workspaces/:id/purge-worktree` from the workspace context menu, or automatically by the pr-watcher when a PR transitions to MERGED **and** `global.autoPurgeOnPrMerged` is enabled (Settings → Worktrees toggle, settings migration v36).
265
334
 
266
- Sequence: `captureRestoreData` (best-effort forge lookup for PR number / URL / merge SHA) → stop agent + dev server + terminal → `archiveWorkspace` → `removeWorktree` → `markWorktreePurged(restoreData)` → emit `workspace:worktree-purged`. Permission errors on removal (EACCES / EPERM, typically Docker-owned files in `node_modules` / `vendor`) are detected via regex on the error message and the warning toast carries a copy-pasteable `sudo rm -rf` + `git worktree prune` recovery command plus prevention tips (Docker `USER` directive, `setfacl` default ACL). See [CONFIGURATION.md → Auto-purge worktree on PR merged](CONFIGURATION.md#auto-purge-worktree-on-pr-merged).
335
+ Sequence: `captureRestoreData` (best-effort forge lookup for PR number / URL / merge SHA) → stop agent + dev server + terminal → `archiveWorkspace` → `removeWorktree` → `markWorktreePurged(restoreData)` → emit `workspace:worktree-purged`. Permission errors on removal (EACCES / EPERM, typically Docker-owned files in `node_modules` / `vendor`) trigger an attempt to reclaim ownership through a temporary Docker container and retry removal. If recovery fails, the UI offers manual cleanup guidance and prevention tips (Docker `USER`, default ACLs). Agent and dev-server shutdown must be confirmed before removal; shutdown failures leave the worktree in place. See [CONFIGURATION.md → Auto-purge worktree on PR merged](CONFIGURATION.md#auto-purge-worktree-on-pr-merged).
267
336
 
268
- **Auto-restore on manual recreation.** When the user manually recreates the worktree folder (`gh pr checkout <pr-number>` or `git worktree add <path> <branch>`), the pr-watcher detects the folder reappearing on its next 30 s tick via `autoRestoreManuallyRecreatedWorktrees()`: it iterates archived workspaces with `worktreePurgedAt`, checks `fs.existsSync(worktreePath)`, and on a hit calls `restoreWorktreeFromDisk(id)` which clears `worktree_purged_at` + `worktree_purge_restore_data` + `archived_at` in one transaction, then emits `workspace:worktree-restored`. The client websocket store reuses the same handler as `workspace:archived`/`unarchived` to refresh both the active and archived workspace lists. No UI action needed.
337
+ **Auto-restore on manual recreation.** When the user manually recreates the worktree folder (`gh pr checkout <pr-number>` or `git worktree add <path> <branch>`), the pr-watcher detects the folder reappearing on its next 30 s tick via `autoRestoreManuallyRecreatedWorktrees()`: it iterates archived workspaces with `worktreePurgedAt`, validates the exact worktree root, repository and working branch via `isMatchingWorkspaceWorktree`, and on a match calls `restoreWorktreeFromDisk(id)` which clears `worktree_purged_at` + `worktree_purge_restore_data` + `archived_at` in one transaction, then emits `workspace:worktree-restored`. The client websocket store reuses the same handler as `workspace:archived`/`unarchived` to refresh both the active and archived workspace lists. No UI action needed.
269
338
 
270
339
  **Restore**: `POST /api/workspaces/:id/restore-worktree` calls `worktree-restore-service.ts` to recreate the exact checkout and only then clear purge/archive metadata via `restoreWorktreeFromDisk`. Source order: surviving local branch → optional `headCommitSha` in the existing restore JSON → exact branch fetched from `origin`. No SQL migration is needed for the optional JSON key; older records remain supported. No reset, overwrite, agent/dev-server start or setup script. `workspace-lifecycle-guard.ts` excludes overlapping restore/purge/delete operations, while the existing common-Git-directory lock serializes Git mutations. The watcher shares `isMatchingWorkspaceWorktree` validation and invalidates in-flight PR checks on restoration; delayed auto-purges carry their expected archive timestamp to avoid purging a workspace restored in the meantime. The context menu and purged banner expose the action; HTTP and the existing `workspace:worktree-restored` event reconcile client lists. Discarded uncommitted/ignored files and dependencies are not recoverable.
271
340
 
@@ -295,7 +364,7 @@ The right panel of `DiffViewer.vue` is editable when the workspace agent is stop
295
364
 
296
365
  **Route layer** is thin: always wrap the handler body in `try / catch` and return `c.json({ error: message }, status)`. Match the existing shape in `src/server/routes/workspaces.ts`.
297
366
 
298
- **Swallowed failures** are acceptable (and required) for best-effort side effects like `agentManager.stopAgent` and `devServerService.stopDevServer` during delete/archive. Log with `console.error` and continue. Never let these break the happy path.
367
+ **Best-effort failures** may be logged and swallowed for non-critical side effects such as lifecycle-hook reporting. Process shutdown is a precondition for destructive operations: use `stopAgentAndWait` with `assertAgentStopped`, propagate dev-server shutdown failures, and retain the worktree when shutdown is unconfirmed. See [Audit remediation contracts](#audit-remediation-contracts-september-2026).
299
368
 
300
369
  **Route ordering matters** in Hono. Static paths (`GET /archived`) MUST be declared **before** dynamic segments (`GET /:id`) or the dynamic segment captures them. There's a regression test locking this invariant in `src/__tests__/routes-workspaces.test.ts`.
301
370
 
@@ -305,7 +374,7 @@ The right panel of `DiffViewer.vue` is editable when the workspace agent is stop
305
374
 
306
375
  ## Internationalization (i18n)
307
376
 
308
- The frontend uses `vue-i18n` v10 with 5 supported locales: English (`en`), French (`fr`), German (`de`), Spanish (`es`), Italian (`it`). Translation files live in `src/client/src/i18n/`.
377
+ The frontend uses `vue-i18n` with 5 supported locales: English (`en`), French (`fr`), German (`de`), Spanish (`es`), Italian (`it`). Translation files live in `src/client/src/i18n/`; the dependency version is declared in `src/client/package.json`.
309
378
 
310
379
  **Mandatory rules for all frontend code:**
311
380
 
@@ -318,9 +387,9 @@ The frontend uses `vue-i18n` v10 with 5 supported locales: English (`en`), Frenc
318
387
 
319
388
  ## Testing discipline
320
389
 
321
- - **TDD for backend**: write the failing test, confirm it fails for the right reason, implement minimally, confirm it passes, commit. One commit per logical unit. See existing tests in `src/__tests__/workspace-service.test.ts` for the setup pattern (fresh in-memory DB per test via `resetDb()`).
390
+ - **TDD for backend**: write the failing test, confirm it fails for the right reason, implement minimally, confirm it passes, commit. One commit per logical unit. See `src/__tests__/workspace-service.test.ts` for the setup pattern: `resetDb()` creates a fresh database in a temporary directory, initializes its schema, and closes/removes it after each test.
322
391
  - **Route tests** use `vi.mock()` on service modules before imports (see `src/__tests__/routes-workspaces.test.ts`). Keep mocks complete; missing exports cause obscure failures.
323
- - **Frontend tests** cover Pinia stores (`workspace-store`, `settings-store`, `templates-store`) and pure utility modules (`expand-template`). Run with `cd src/client && npm test`. Vue components are **not** tested: type-check via `npx tsc --noEmit` + manual smoke testing covers UI behavior.
392
+ - **Frontend tests** cover Pinia stores, pure utilities, composables, and selected Vue components (for example `ActivityFeed`, `TurnCard`, and `WorkspaceCard`). Run with `cd src/client && npm test`; use Vue Test Utils and the existing `happy-dom` setup for component regressions. Run `cd src/client && npm run type-check` for Vue/TypeScript checking, and use manual smoke tests for browser behavior that the DOM test environment cannot exercise.
324
393
  - **`beforeEach(() => vi.clearAllMocks())`** is the convention for all route test files.
325
394
 
326
395
  ## Git workflow
@@ -385,7 +454,7 @@ deviate from the documented system without explicit user approval.
385
454
  - Don't add confirmation dialogs for reversible actions (archive, unarchive). Only destructive actions (delete) get a dialog.
386
455
  - Don't introduce ORMs, query builders, or schema validation libraries; the project is small enough for raw prepared statements and hand-written mappers.
387
456
  - Don't break the single-source-of-truth of `CLAUDE.md` → `AGENTS.md` symlink. Edit `AGENTS.md`; `CLAUDE.md` follows automatically.
388
- - Don't skip `try/catch` swallowing on best-effort cleanup (agent stop, dev-server stop, worktree removal). These must never break the primary operation.
457
+ - Don't swallow failed or timed-out shutdowns before deleting, purging, or replacing a worktree. Only non-critical best-effort side effects may fail without blocking the primary operation.
389
458
  - 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).
390
459
  - 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).
391
460
 
package/CHANGELOG.md CHANGED
@@ -4,6 +4,14 @@ 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.41
8
+
9
+ - feat(attachments): support documents in workspace creation and chat
10
+
11
+ ## 1.11.40
12
+
13
+ - feat(workspaces): add external MCP access and harden workspace flows
14
+
7
15
  ## 1.11.39
8
16
 
9
17
  - fix(deps): require Claude SDK with Fable 5.1 support
package/README.md CHANGED
@@ -38,14 +38,16 @@ Default port is `3000`; if it's taken, `SERVER_PORT` (checked first) or `PORT` p
38
38
  SERVER_PORT=9997 PORT=9998 npx @loicngr/kobo@latest
39
39
  ```
40
40
 
41
- Kōbō's production build is an installable PWA — use your browser's **Install app** action. Want to run from source or contribute instead? See [`CONTRIBUTING.md`](./CONTRIBUTING.md). Prefer Docker? Jump to [Docker deployment](#docker-deployment).
41
+ Kōbō's production build is an installable PWA — use your browser's **Install app** action. Want to run from source or contribute instead? See [`CONTRIBUTING.md`](./CONTRIBUTING.md). Prefer Docker? Jump to [Docker deployment](#docker).
42
42
 
43
43
  ![Diff viewer with side-by-side changes](docs/assets/images/diff-viewer.png)
44
44
 
45
45
  ## Everything else Kōbō does
46
46
 
47
+ - **Attachments from the first prompt**: paste, drop or attach screenshots, Markdown, text files or PDFs to the creation description or workspace chat. Kōbō copies them into the worktree for the agent. See [creation attachments](./CONFIGURATION.md#attachments-in-the-creation-description) and [chat attachments](./CONFIGURATION.md#attachments-in-workspace-chat).
48
+
47
49
  - **Command palette & search**: `/` autocompletes skills and commands, `@` fuzzy-completes worktree file paths, `Ctrl+F` searches readable messages, `Ctrl+K` opens a command palette. The global search page deep-links to the same spot.
48
- - **Full MCP toolset (`kobo-tasks`)**: task/acceptance-criteria CRUD, dev server control, a unified `get_ticket` (Notion or Sentry), cross-workspace conversation search, per-session usage, and a `.ai/thoughts` decision log — see [`AGENTS.md`](./AGENTS.md) for the full tool list.
50
+ - **MCP toolset (`kobo-tasks`)**: task/acceptance-criteria CRUD, dev server control, a unified `get_ticket` (Notion or Sentry), cross-workspace conversation search, per-session usage, and a `.ai/thoughts` decision log — see the [MCP guide](./src/mcp-server/README.md#tools). External MCP clients can also discover workspaces, read conversations, send messages, and answer questions through HTTP or global stdio.
49
51
 
50
52
  ![Sub-agents panel showing parallel tool calls](docs/assets/images/sub-agents-panel.png)
51
53
  - **Multi-forge Git panel**: GitHub (`gh`), GitLab (`glab`), or Bitbucket Community (`bkt`), auto-detected from the remote — `Open PR`, `Merge ready PR`, `Change PR base`, `Change source branch`, all from the UI.
@@ -56,7 +58,7 @@ Kōbō's production build is an installable PWA — use your browser's **Install
56
58
  ![Agent asking a clarifying question, awaiting the user's answer](docs/assets/images/agent-question.png)
57
59
  - **Quota-aware**: 5-hour / 7-day Claude usage and Codex rate-limit buckets live in the footer; sessions auto-resume after a reset.
58
60
  - **Disk-space purge**: reclaim a merged workspace's `node_modules`/`vendor` weight without losing its chat history — see [`CONFIGURATION.md`](./CONFIGURATION.md#auto-purge-worktree-on-pr-merged).
59
- - **Lifecycle scripts**: shell scripts run automatically on setup, cleanup, and archive, with output streamed into the chat.
61
+ - **Lifecycle scripts**: shell scripts run on setup, cleanup, archive, session end, PR merge, or auto-loop stop, with output streamed into the chat.
60
62
  - **Observability**: a per-session timeline (duration, tools, tokens, errors) and a downloadable redacted diagnostic JSON.
61
63
  - **Optional integrations**: Notion (import missions) and Sentry (fix from issue URL), each independently toggled with a **Test connection** action; local voice transcription via `whisper.cpp`.
62
64
 
@@ -105,7 +107,7 @@ src/
105
107
  ├── client/ # Vue 3 + Quasar PWA
106
108
  ├── mcp-server/ # kobo-tasks MCP server, spawned per workspace
107
109
  ├── shared/ # types shared backend ↔ frontend
108
- └── __tests__/ # Vitest suite, backend + client, thousands of tests
110
+ └── __tests__/ # backend Vitest suite; client tests live in client/src/__tests__/
109
111
  ```
110
112
 
111
113
  [`AGENTS.md`](./AGENTS.md) covers the data model, WebSocket protocol, engine contracts, MCP tool surface, migration discipline, i18n rules, and contribution guidelines.
@@ -4,7 +4,7 @@ import { nanoid } from 'nanoid';
4
4
  import * as cronService from '../server/services/cron-service.js';
5
5
  import * as settingsService from '../server/services/settings-service.js';
6
6
  import { slugifyProjectName } from '../server/utils/project-slug.js';
7
- import { resolveExistingPathInside } from '../server/utils/safe-path.js';
7
+ import { ensureDirectoryInside, resolveExistingPathInside } from '../server/utils/safe-path.js';
8
8
  import { resolveWorkspaceWorktreePath } from '../server/utils/worktree-paths.js';
9
9
  import { MASKED_SECRET, SECRET_GLOBAL_KEYS } from '../shared/consts.js';
10
10
  /** Allowed task status values. */
@@ -404,7 +404,7 @@ export function readDocumentHandler(worktreePath, relPath) {
404
404
  }
405
405
  /**
406
406
  * Append a thought / decision / note to
407
- * `.ai/thoughts/logs/<YYYY-MM-DD>-<slug>.md`. Creates the directory if missing.
407
+ * `.ai/thoughts/logs/<YYYY-MM-DD>-<slug>-<timestamp>-<id>.md`. Creates the directory if missing.
408
408
  * Returns the path (worktree-relative) of the file actually written — useful
409
409
  * for the agent to reference it in chat.
410
410
  *
@@ -419,8 +419,7 @@ export function logThoughtHandler(worktreePath, data) {
419
419
  const content = data.content?.trim();
420
420
  if (!content)
421
421
  throw new Error('content is required');
422
- const thoughtsDir = path.join(worktreePath, '.ai', 'thoughts', 'logs');
423
- fs.mkdirSync(thoughtsDir, { recursive: true });
422
+ const thoughtsDir = ensureDirectoryInside(worktreePath, path.join('.ai', 'thoughts', 'logs'));
424
423
  const date = new Date().toISOString().slice(0, 10);
425
424
  const slug = title
426
425
  .toLowerCase()
@@ -430,12 +429,20 @@ export function logThoughtHandler(worktreePath, data) {
430
429
  .replace(/^-+|-+$/g, '')
431
430
  .slice(0, 60) || 'note';
432
431
  const tagSuffix = data.tag ? `-${data.tag.replace(/[^a-z0-9]+/gi, '-').toLowerCase()}` : '';
433
- const filename = `${date}-${slug}${tagSuffix}.md`;
434
- const abs = path.join(thoughtsDir, filename);
435
- const relPath = `.ai/thoughts/logs/${filename}`;
436
432
  const header = `# ${title}\n\n_${new Date().toISOString()}_${data.tag ? ` · tag: \`${data.tag}\`` : ''}\n\n`;
437
- fs.writeFileSync(abs, header + content + (content.endsWith('\n') ? '' : '\n'), 'utf-8');
438
- return { path: relPath };
433
+ for (let attempt = 0; attempt < 5; attempt++) {
434
+ const filename = `${date}-${slug}${tagSuffix}-${Date.now()}-${nanoid(10)}.md`;
435
+ const abs = path.join(thoughtsDir, filename);
436
+ try {
437
+ fs.writeFileSync(abs, header + content + (content.endsWith('\n') ? '' : '\n'), { encoding: 'utf-8', flag: 'wx' });
438
+ return { path: `.ai/thoughts/logs/${filename}` };
439
+ }
440
+ catch (err) {
441
+ if (err.code !== 'EEXIST')
442
+ throw err;
443
+ }
444
+ }
445
+ throw new Error('Could not allocate a unique thought filename');
439
446
  }
440
447
  /**
441
448
  * Aggregate `usage` events from `ws_events` to report how many tokens and
@@ -3,7 +3,9 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
4
  import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
5
5
  import { getDb } from '../server/db/index.js';
6
+ import { WORKSPACE_DIALOGUE_TOOLS } from '../shared/workspace-dialogue-tools.js';
6
7
  import { createTaskHandler, cronListHandler, deleteTaskHandler, getDevServerStatusHandler, getSessionUsageHandler, getSettingsHandler, getTicketSourcesHandler, getWorkspaceInfoHandler, listDocumentsHandler, listTasksHandler, listWorkspaceImagesHandler, listWorkspacesHandler, logThoughtHandler, markAutoLoopReadyHandler, markTaskDoneHandler, readDocumentHandler, readWorkspaceEventsCsvHandler, setWorkspaceAgentDescriptionHandler, updateTaskHandler, } from './kobo-tasks-handlers.js';
8
+ import { callWorkspaceDialogueTool } from './workspace-dialogue-client.js';
7
9
  const workspaceId = process.env.KOBO_WORKSPACE_ID;
8
10
  const dbPath = process.env.KOBO_DB_PATH;
9
11
  const settingsPath = process.env.KOBO_SETTINGS_PATH;
@@ -94,9 +96,12 @@ async function notifyAgentDescriptionUpdated() {
94
96
  /** Generic HTTP request to the Kobo backend, returning parsed JSON or null. */
95
97
  async function backendRequest(method, pathname, body) {
96
98
  const url = `${backendUrl}${pathname}`;
97
- const init = { method };
99
+ const headers = process.env.KOBO_NETWORK_TOKEN
100
+ ? { 'X-Kobo-Token': process.env.KOBO_NETWORK_TOKEN }
101
+ : {};
102
+ const init = { method, headers };
98
103
  if (body !== undefined) {
99
- init.headers = { 'Content-Type': 'application/json' };
104
+ headers['Content-Type'] = 'application/json';
100
105
  init.body = JSON.stringify(body);
101
106
  }
102
107
  const res = await fetch(url, init);
@@ -475,6 +480,7 @@ const WORKSPACE_SCOPED_TOOLS = [
475
480
  },
476
481
  ];
477
482
  const GLOBAL_TOOLS = [
483
+ ...WORKSPACE_DIALOGUE_TOOLS,
478
484
  {
479
485
  name: 'list_workspaces',
480
486
  description: 'List Kōbō workspaces with their id, title, status, and creation date. Works even when the Kōbō backend server is not running (reads the database directly). Use this to discover existing workspaces before creating one or targeting one with archive_workspace/stop_workspace.',
@@ -594,6 +600,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
594
600
  return fail(`Tool '${name}' requires a workspace-bound MCP session (KOBO_WORKSPACE_ID not set).`);
595
601
  }
596
602
  try {
603
+ if (WORKSPACE_DIALOGUE_TOOLS.some((tool) => tool.name === name)) {
604
+ if (workspaceId && a.workspace_id === workspaceId && name === 'send_workspace_message')
605
+ return fail('Use the normal conversation to communicate in your own workspace.');
606
+ return await callWorkspaceDialogueTool(backendUrl, name, a, process.env.KOBO_NETWORK_TOKEN, process.env.KOBO_MCP_CLIENT_NAME ?? server.getClientVersion()?.name);
607
+ }
597
608
  if (name === 'list_tasks') {
598
609
  return ok(listTasksHandler(db, workspaceId));
599
610
  }
@@ -0,0 +1,28 @@
1
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
3
+ import { validateDialogueArguments } from '../shared/workspace-dialogue-tools.js';
4
+ import { normalizeClientName } from '../shared/workspace-message-types.js';
5
+ /** Stateless HTTP bridge: external stdio clients never own agent runtime state. */
6
+ export async function callWorkspaceDialogueTool(backendUrl, name, args, token, clientName) {
7
+ const arguments_ = validateDialogueArguments(name, args);
8
+ const client = new Client({ name: 'kobo-stdio-bridge', version: '1.0.0' });
9
+ const signal = AbortSignal.timeout(30_000);
10
+ const transport = new StreamableHTTPClientTransport(new URL(`${backendUrl.replace(/\/$/, '')}/api/mcp`), {
11
+ requestInit: {
12
+ signal,
13
+ headers: {
14
+ ...(token ? { 'X-Kobo-Token': token } : {}),
15
+ 'X-Kobo-Client-Name': encodeURIComponent(normalizeClientName(clientName)),
16
+ 'X-Kobo-Client-Name-Encoding': 'uri',
17
+ 'X-Kobo-Client-Transport': 'stdio',
18
+ },
19
+ },
20
+ });
21
+ try {
22
+ await client.connect(transport, { signal });
23
+ return await client.callTool({ name, arguments: arguments_ }, undefined, { signal });
24
+ }
25
+ finally {
26
+ await client.close();
27
+ }
28
+ }
@@ -0,0 +1,16 @@
1
+ /** v43 schema shared by fresh installs and the append-only migration. */
2
+ export function initMcpMessageSchema(db) {
3
+ db.exec(`CREATE TABLE IF NOT EXISTS mcp_message_requests (
4
+ workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
5
+ idempotency_key TEXT NOT NULL,
6
+ request_id TEXT NOT NULL UNIQUE,
7
+ fingerprint TEXT NOT NULL,
8
+ state TEXT NOT NULL CHECK (state IN ('reserved','dispatching','accepted','rejected','unknown')),
9
+ result_json TEXT,
10
+ event_id TEXT,
11
+ session_id TEXT,
12
+ created_at TEXT NOT NULL,
13
+ updated_at TEXT NOT NULL,
14
+ PRIMARY KEY (workspace_id, idempotency_key)
15
+ )`);
16
+ }
@@ -1,4 +1,6 @@
1
+ import { initMcpMessageSchema } from './mcp-message-schema.js';
1
2
  import { initSchema } from './schema.js';
3
+ import { initSearchSchema } from './search-schema.js';
2
4
  /** Ordered registry of all schema migrations. Append new entries at the end. */
3
5
  export const migrations = [
4
6
  {
@@ -740,6 +742,8 @@ export const migrations = [
740
742
  CREATE INDEX IF NOT EXISTS idx_workspace_activity_created ON workspace_activity(created_at);`);
741
743
  },
742
744
  },
745
+ { version: 42, name: 'index-logical-search-messages', migrate: initSearchSchema },
746
+ { version: 43, name: 'durable-mcp-message-requests', migrate: initMcpMessageSchema },
743
747
  ];
744
748
  /** Current schema version — always equals the highest migration version. */
745
749
  export const SCHEMA_VERSION = migrations.length > 0 ? migrations[migrations.length - 1].version : 1;
@@ -1,3 +1,5 @@
1
+ import { initMcpMessageSchema } from './mcp-message-schema.js';
2
+ import { initSearchSchema } from './search-schema.js';
1
3
  /** Create all tables and indexes for a fresh install. Not used for upgrades -- see migrations.ts. */
2
4
  export function initSchema(db) {
3
5
  db.exec(`
@@ -221,4 +223,6 @@ export function initSchema(db) {
221
223
  CREATE INDEX IF NOT EXISTS idx_session_event_metrics_session
222
224
  ON session_event_metrics(session_id);
223
225
  `);
226
+ initSearchSchema(db);
227
+ initMcpMessageSchema(db);
224
228
  }