@loicngr/kobo 1.7.16 → 1.7.18

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 (86) hide show
  1. package/AGENTS.md +35 -4
  2. package/CHANGELOG.md +12 -0
  3. package/README.md +2 -1
  4. package/dist/mcp-server/kobo-tasks-handlers.js +48 -5
  5. package/dist/mcp-server/kobo-tasks-server.js +8 -20
  6. package/dist/server/db/migrations.js +19 -0
  7. package/dist/server/db/schema.js +9 -0
  8. package/dist/server/routes/settings.js +2 -0
  9. package/dist/server/routes/workspaces.js +242 -56
  10. package/dist/server/services/agent/engines/claude-code/engine.js +51 -7
  11. package/dist/server/services/agent/engines/claude-code/options-builder.js +2 -2
  12. package/dist/server/services/agent/engines/codex/options-builder.js +2 -2
  13. package/dist/server/services/agent/orchestrator.js +1 -0
  14. package/dist/server/services/change-source-branch-service.js +150 -0
  15. package/dist/server/services/chat-history-service.js +41 -0
  16. package/dist/server/services/file-editor-service.js +59 -0
  17. package/dist/server/services/forge/github/provider.js +121 -0
  18. package/dist/server/services/forge/gitlab/provider.js +178 -0
  19. package/dist/server/services/forge/none.js +23 -0
  20. package/dist/server/services/forge/registry.js +17 -0
  21. package/dist/server/services/forge/resolve.js +34 -0
  22. package/dist/server/services/forge/types.js +9 -0
  23. package/dist/server/services/git-stats-service.js +32 -0
  24. package/dist/server/services/pr-watcher-service.js +33 -3
  25. package/dist/server/services/settings-defaults.js +77 -0
  26. package/dist/server/services/settings-service.js +34 -0
  27. package/dist/server/utils/git-ops.js +121 -134
  28. package/package.json +1 -1
  29. package/src/client/dist/spa/assets/ActivityFeed-DHf6rSgl.js +8 -0
  30. package/src/client/dist/spa/assets/{ClosePopup-CxvZA3ft.js → ClosePopup-A-tSm4aa.js} +1 -1
  31. package/src/client/dist/spa/assets/{CreatePage-CdZr7f3j.js → CreatePage-DL8LTcyD.js} +1 -1
  32. package/src/client/dist/spa/assets/DiffViewer-C4L5y8Ho.css +1 -0
  33. package/src/client/dist/spa/assets/DiffViewer-CgzuQueb.js +8 -0
  34. package/src/client/dist/spa/assets/{HealthPage-z1uIOpYk.js → HealthPage-Zsnyyv66.js} +1 -1
  35. package/src/client/dist/spa/assets/{MainLayout-BJmBXwYn.css → MainLayout-KEr19FOv.css} +1 -1
  36. package/src/client/dist/spa/assets/MainLayout-OJcuFEwx.js +37 -0
  37. package/src/client/dist/spa/assets/{QExpansionItem-BTd5m2yV.js → QExpansionItem-CgJQdznK.js} +1 -1
  38. package/src/client/dist/spa/assets/{QMenu-C2Wwwf2E.js → QMenu-NVDU7D3u.js} +1 -1
  39. package/src/client/dist/spa/assets/{QScrollArea-A1wI0IXU.js → QScrollArea-DFNGAP1T.js} +1 -1
  40. package/src/client/dist/spa/assets/{QTooltip-Bfdmzm_m.js → QTooltip-BC7PnZJ1.js} +1 -1
  41. package/src/client/dist/spa/assets/{SearchPage-ChmKHNKn.js → SearchPage-CpmeT5hL.js} +1 -1
  42. package/src/client/dist/spa/assets/{SettingsPage-BJLyYrBN.css → SettingsPage-BTGPZaqC.css} +1 -1
  43. package/src/client/dist/spa/assets/SettingsPage-CKz2kdw8.js +9 -0
  44. package/src/client/dist/spa/assets/{TouchPan-BIE5rs7U.js → TouchPan-D0fJnlOC.js} +1 -1
  45. package/src/client/dist/spa/assets/WorkspacePage-CcWa3--k.js +4 -0
  46. package/src/client/dist/spa/assets/WorkspacePage-DPj03Um2.css +1 -0
  47. package/src/client/dist/spa/assets/{build-path-tree-BGUV3nY1.js → build-path-tree-CyqReJkk.js} +1 -1
  48. package/src/client/dist/spa/assets/{cssMode-BU4X8R6a.js → cssMode-DKW40Eay.js} +1 -1
  49. package/src/client/dist/spa/assets/{editor.api-B4xBDzmJ.js → editor.api-cIZo-p3R.js} +1 -1
  50. package/src/client/dist/spa/assets/{editor.main-CSZRkloL.js → editor.main-DxYwm0in.js} +3 -3
  51. package/src/client/dist/spa/assets/{engineFeatures-CLOVr5b4.js → engineFeatures-vEC-j3xd.js} +1 -1
  52. package/src/client/dist/spa/assets/{expand-template-BxUkuL5g.js → expand-template-Bmbq9pxX.js} +1 -1
  53. package/src/client/dist/spa/assets/{freemarker2-DRz20wAV.js → freemarker2-DYe7YniO.js} +1 -1
  54. package/src/client/dist/spa/assets/{handlebars-C0dsvPnC.js → handlebars-CFHnjuEe.js} +1 -1
  55. package/src/client/dist/spa/assets/{html-Cqvj1pWs.js → html-D_DPVIcT.js} +1 -1
  56. package/src/client/dist/spa/assets/{htmlMode-BTHNvkm6.js → htmlMode-CULL5FkI.js} +1 -1
  57. package/src/client/dist/spa/assets/i18n-awaKh__J.js +1 -0
  58. package/src/client/dist/spa/assets/index-B2qdU9v-.js +52 -0
  59. package/src/client/dist/spa/assets/{javascript-C8n3U02v.js → javascript-Cj-bhbPb.js} +1 -1
  60. package/src/client/dist/spa/assets/{jsonMode-C3AFxQ6K.js → jsonMode-CJrCPpxd.js} +1 -1
  61. package/src/client/dist/spa/assets/{kobo-commands-BuxgteGZ.js → kobo-commands-B2AhWe1S.js} +1 -1
  62. package/src/client/dist/spa/assets/{liquid-C4wtUDrJ.js → liquid-B4ttnSVX.js} +1 -1
  63. package/src/client/dist/spa/assets/{mdx-CaT1p1F2.js → mdx-DT9HeWQS.js} +1 -1
  64. package/src/client/dist/spa/assets/{monaco.contribution-CJg5GKVf.js → monaco.contribution-Dc3R0xb9.js} +2 -2
  65. package/src/client/dist/spa/assets/{notifications-BC6en6Lt.js → notifications-Hq-6rEYv.js} +1 -1
  66. package/src/client/dist/spa/assets/{permissionModes-BQHBTBwa.js → permissionModes-BA0XHeew.js} +1 -1
  67. package/src/client/dist/spa/assets/{python-Cj54W2Tg.js → python-DSdYwb75.js} +1 -1
  68. package/src/client/dist/spa/assets/{razor-D3gJxoX_.js → razor-C-5bSEPf.js} +1 -1
  69. package/src/client/dist/spa/assets/{render-chat-markdown-DxEHr3lW.js → render-chat-markdown-cMOd2guW.js} +1 -1
  70. package/src/client/dist/spa/assets/{tsMode-B6S4PLWH.js → tsMode-Bck0IzqV.js} +1 -1
  71. package/src/client/dist/spa/assets/{typescript-Ca8AEX3t.js → typescript-Daj2xIGr.js} +1 -1
  72. package/src/client/dist/spa/assets/{use-onboarding-CNeLPDtv.js → use-onboarding-Xp0y257M.js} +1 -1
  73. package/src/client/dist/spa/assets/{xml-CsKo4k8C.js → xml-DLN-RVL8.js} +1 -1
  74. package/src/client/dist/spa/assets/{yaml-X5yKmi6z.js → yaml-DnDRs7J6.js} +1 -1
  75. package/src/client/dist/spa/index.html +2 -2
  76. package/src/mcp-server/kobo-tasks-handlers.ts +56 -5
  77. package/src/mcp-server/kobo-tasks-server.ts +8 -19
  78. package/src/client/dist/spa/assets/ActivityFeed-DU6lDEP0.js +0 -8
  79. package/src/client/dist/spa/assets/DiffViewer-DTdDcKZC.css +0 -1
  80. package/src/client/dist/spa/assets/DiffViewer-m801GPfI.js +0 -7
  81. package/src/client/dist/spa/assets/MainLayout-oJdQ-QKM.js +0 -37
  82. package/src/client/dist/spa/assets/SettingsPage-B59LoCos.js +0 -9
  83. package/src/client/dist/spa/assets/WorkspacePage-Bj1PJSWT.js +0 -4
  84. package/src/client/dist/spa/assets/WorkspacePage-tFBswKV9.css +0 -1
  85. package/src/client/dist/spa/assets/i18n-D1I-Us2H.js +0 -1
  86. package/src/client/dist/spa/assets/index-KABmOIkF.js +0 -2
package/AGENTS.md CHANGED
@@ -62,7 +62,11 @@ src/
62
62
  │ │ │ ├── orchestrator.ts # per-workspace engine map, retry/quota handling, watchdog, public API
63
63
  │ │ │ ├── session-controller.ts # lifecycle wrapper around an AgentEngine instance
64
64
  │ │ │ ├── event-router.ts # maps engine AgentEvent stream to WS emit + DB side-effects
65
- │ │ │ └── engines/claude-code/ # Claude Code engine (spawn + stream-parser + args-builder + mcp-config + capabilities)
65
+ │ │ │ └── engines/{claude-code,codex}/ # per-engine adapters implementing the AgentEngine contract
66
+ │ │ ├── forge/ # ForgeProvider abstraction — github / gitlab / none, with registry + auto-resolve
67
+ │ │ ├── change-source-branch-service.ts # re-target a workspace onto a new source branch (built-in cherry-pick or custom bash)
68
+ │ │ ├── git-stats-service.ts # pure compute of commit/ahead-behind/diff stats + forge availability for a workspace
69
+ │ │ ├── settings-defaults.ts # DEFAULT_* constants for opt-in settings (e.g. change-source-branch script)
66
70
  │ │ ├── content-migration-service.ts # runtime legacy ws_events → normalised AgentEvent migration
67
71
  │ │ ├── templates-service.ts # prompt templates CRUD (JSON file persistence, seeding)
68
72
  │ │ ├── dev-server-service.ts # per-workspace dev server lifecycle (docker or npm process)
@@ -82,8 +86,8 @@ src/
82
86
  ├── client/ # Vue 3 + Quasar SPA
83
87
  │ └── src/
84
88
  │ ├── stores/ # pinia: workspace, websocket, settings, dev-server, templates
85
- │ ├── components/ # WorkspaceList, NotionPanel, AcceptancePanel, ChatInput, GitPanel, PlansPanel…
86
- │ ├── utils/ # expand-template (template variable substitution), formatters
89
+ │ ├── components/ # WorkspaceList, NotionPanel, AcceptancePanel, ChatInput, GitPanel, PlansPanel, WorkspaceAttentionLabels
90
+ │ ├── utils/ # expand-template, formatters, workspace-attention (CI failure / changes-requested derivation)…
87
91
  │ ├── pages/ # WorkspacePage, CreatePage, SettingsPage
88
92
  │ └── router/
89
93
  ├── mcp-server/ # standalone MCP server spawned per workspace
@@ -101,6 +105,7 @@ src/
101
105
  | `agent_sessions` | Claude Code CLI invocations — pid, `claude_session_id`, status, started_at, ended_at, `name` |
102
106
  | `ws_events` | persisted WebSocket events for replay on reconnect — type, payload, session_id, created_at |
103
107
  | `pending_wakeups` | one-row-per-workspace scheduler for the `ScheduleWakeup` tool — target_at (ISO UTC), prompt, reason; CASCADE DELETE on workspace |
108
+ | `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 |
104
109
 
105
110
  `status` enum: `created | extracting | brainstorming | executing | completed | idle | error | quota`. Transitions are validated in `updateWorkspaceStatus` against `VALID_TRANSITIONS`.
106
111
 
@@ -157,6 +162,10 @@ Two emit flavors in `websocket-service.ts`:
157
162
 
158
163
  ## External integrations
159
164
 
165
+ ### Forge providers
166
+
167
+ `src/server/services/forge/` implements a `ForgeProvider` interface with three concrete providers: `github` (wraps the `gh` CLI), `gitlab` (wraps the `glab` CLI), and `none` (no-op — 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** — 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.
168
+
160
169
  ### Notion (opt-in, user-provided credentials)
161
170
 
162
171
  `notion-service.ts` spawns the official [`@notionhq/notion-mcp-server`](https://github.com/makenotion/notion-mcp-server) as a child process (`npx -y @notionhq/notion-mcp-server`) and talks to it over stdio using JSON-RPC / MCP. **Kōbō ships no Notion credentials** — the feature only works if the user has configured their own integration token. The token is resolved in this order:
@@ -175,7 +184,7 @@ See the "Notion integration" section of the README for the end-user setup guide.
175
184
 
176
185
  Two engines live under `src/server/services/agent/engines/`, both implementing the `AgentEngine` contract in `types.ts`:
177
186
 
178
- **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.
187
+ **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`.
179
188
 
180
189
  **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:
181
190
  - `jsonrpc/transport.ts` + `jsonrpc/peer.ts` — generic JSON-RPC 2.0 stdio peer (request correlation, notifications, server-initiated requests)
@@ -200,6 +209,28 @@ Background: the engine was migrated from `@openai/codex-sdk` (one-shot `codex ex
200
209
  - **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.
201
210
  - **`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.
202
211
 
212
+ ## Workspace operations
213
+
214
+ ### Change source branch
215
+
216
+ `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`.
217
+
218
+ - **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.
219
+ - **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 = feature disabled (opt-in).
220
+ - **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).
221
+
222
+ ### Workspace attention indicators
223
+
224
+ `src/client/src/utils/workspace-attention.ts` derives a small set of badges (CI failure, changes-requested) from the PR snapshot + git stats stored on each workspace. `WorkspaceAttentionLabels.vue` renders them inline on the workspace cards in the left drawer. The derivation is a pure function — easy to unit-test, no IO. Drawer cards therefore stay reactive to whatever the pr-watcher / bulk-info refresh writes back into the store.
225
+
226
+ ### Bulk workspace info refresh
227
+
228
+ `GET /api/workspaces/info` returns `{ workspaces, prSnapshots, gitStats }` in one shot. The client polls this endpoint every 30 s so every non-archived workspace stays ≤ 30 s fresh without a per-card stats request. The server-side pr-watcher feeds the same caches (`lastKnownGitStats` map, PR snapshots) so the work is shared between the polling client and the watchdog loop.
229
+
230
+ ### File editing in the diff viewer
231
+
232
+ The right panel of `DiffViewer.vue` is editable when the workspace agent is stopped and the file is not in `deleted` status. `Ctrl/Cmd+S` or the explicit Save button persists the file via `POST /api/workspaces/:id/save-file`, sending `{ path, content, baseSha }` where `baseSha` is the sha256 captured at `GET /diff-file` time. The route refuses with **412 Precondition Failed** + `{ currentSha }` if the on-disk content has changed; the client shows a Reload / Keep mine dialog. Worktree-traversal guards (including parent-symlink escapes) and a 1 MB size cap live in `file-editor-service.ts`. Changing files in the tree while dirty pops an "Unsaved changes" prompt.
233
+
203
234
  ## Code conventions
204
235
 
205
236
  **Service layer** throws descriptive errors; the route layer catches and maps to HTTP status codes. Error messages follow the pattern `` `Workspace '${id}' not found` `` / `` `... is already archived` ``.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,18 @@ 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.7.18
8
+
9
+ - chore(audit): fix npm audit
10
+ - feat(client): collapsible ask-user-question panel
11
+ - feat: per-workspace chat history + inline file editing in the diff viewer
12
+ - feat: multi-forge, change source branch, workspace attention
13
+
14
+ ## 1.7.17
15
+
16
+ - feat: per-workspace chat history + inline file editing in the diff viewer
17
+ - feat: multi-forge, change source branch, pr-watcher
18
+
7
19
  ## 1.7.16
8
20
 
9
21
  - feat(engine): handle user interruptions as clean stops
package/README.md CHANGED
@@ -17,7 +17,8 @@ Kōbō runs multiple coding agents in parallel, each isolated in its own git wor
17
17
  - **Two agent engines** — Claude Code (via `@anthropic-ai/claude-agent-sdk`) and OpenAI Codex (via `codex app-server`), chosen per workspace.
18
18
  - **Live chat** — streaming text, reasoning blocks, inline Edit/Write diffs, per-turn cards, infinite scrollback; `/` autocompletes skills & commands and `@` fuzzy-autocompletes worktree file paths; every workspace's session events are exportable to CSV.
19
19
  - **Task tracking** — per-workspace MCP server (`kobo-tasks`) lets the agent manage its own tasks, acceptance criteria, and live status.
20
- - **Git panel** — Monaco-based diff viewer, inline conflict resolution, `Sync` / `Push` / `Open PR` wired to the `gh` CLI.
20
+ - **Git panel** — 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, `Sync` / `Push` / `Open PR` / `Change PR base` / `Change source branch` (cherry-pick of the branch-proper commits, with an optional custom bash script). Multi-forge: GitHub (`gh`), GitLab (`glab`), or no forge — auto-detected from the remote, overridable per project.
21
+ - **Attention indicators** — workspace cards in the drawer surface CI failures and review-requested-changes inline, so failing PRs/MRs stand out at a glance.
21
22
  - **Auto-loop** — opt-in mode that walks the task list, spawning a fresh session per task and stopping on completion, stall, or error.
22
23
  - **Quota-aware** — 5-hour / 7-day Claude usage and Codex rate-limit buckets in the footer; sessions auto-resume after a rate-limit reset.
23
24
  - **Scheduled wakeups** — the agent schedules a one-shot wake-up via the `ScheduleWakeup` tool; Kōbō persists it across restarts, shows a live countdown, and re-invokes the agent with the stored prompt at the chosen time.
@@ -235,6 +235,44 @@ export function listWorkspaceImagesHandler(worktreePath) {
235
235
  };
236
236
  });
237
237
  }
238
+ /**
239
+ * `## Source` block markers written by the workspace creation route — a
240
+ * `- Notion: <url>` line for Notion imports, `- Sentry: <url>` for Sentry.
241
+ * The marker is the discriminator: it is what tells a ticket file apart from
242
+ * an agent note, and Notion apart from Sentry.
243
+ */
244
+ const SOURCE_MARKER = /^- (Notion|Sentry): (.+)$/m;
245
+ /**
246
+ * Read the mission's source-of-truth ticket(s). Both the Notion and Sentry
247
+ * importers write their extracted brief into `.ai/thoughts/` at workspace
248
+ * creation; this returns one entry per ticket file, typed by origin.
249
+ *
250
+ * Only `.md` files at the *root* of `.ai/thoughts/` are considered — agent
251
+ * notes live under `.ai/thoughts/logs/` and are deliberately skipped. Files
252
+ * with no recognised `## Source` marker (stray notes) are skipped too.
253
+ * Sorted by `type` for a deterministic order. Empty array when nothing matches.
254
+ */
255
+ export function getTicketSourcesHandler(worktreePath) {
256
+ const thoughtsDir = path.join(worktreePath, '.ai', 'thoughts');
257
+ if (!fs.existsSync(thoughtsDir))
258
+ return [];
259
+ const sources = [];
260
+ for (const entry of fs.readdirSync(thoughtsDir, { withFileTypes: true })) {
261
+ if (!entry.isFile() || !entry.name.endsWith('.md'))
262
+ continue;
263
+ const content = fs.readFileSync(path.join(thoughtsDir, entry.name), 'utf-8');
264
+ const match = content.match(SOURCE_MARKER);
265
+ if (!match)
266
+ continue;
267
+ sources.push({
268
+ type: match[1].toLowerCase(),
269
+ url: match[2].trim() || null,
270
+ content: content.trim(),
271
+ });
272
+ }
273
+ sources.sort((a, b) => a.type.localeCompare(b.type));
274
+ return sources;
275
+ }
238
276
  // ── Documents ────────────────────────────────────────────────────────────────
239
277
  /** Directories (relative to the worktree root) scanned for AI-generated docs. */
240
278
  export const DOCUMENT_DIRS = ['docs/plans', 'docs/superpowers', '.ai/thoughts'];
@@ -308,9 +346,14 @@ export function readDocumentHandler(worktreePath, relPath) {
308
346
  return { path: normalized, content: fs.readFileSync(abs, 'utf-8') };
309
347
  }
310
348
  /**
311
- * Append a thought / decision / note to `.ai/thoughts/<YYYY-MM-DD>-<slug>.md`.
312
- * Creates the directory if missing. Returns the path (worktree-relative) of
313
- * the file actually written — useful for the agent to reference it in chat.
349
+ * Append a thought / decision / note to
350
+ * `.ai/thoughts/logs/<YYYY-MM-DD>-<slug>.md`. Creates the directory if missing.
351
+ * Returns the path (worktree-relative) of the file actually written — useful
352
+ * for the agent to reference it in chat.
353
+ *
354
+ * Notes live in the `logs/` sub-directory so they stay separate from the
355
+ * mission's source-of-truth ticket files at the root of `.ai/thoughts/` —
356
+ * `get_ticket` reads only the root and therefore never picks up agent notes.
314
357
  */
315
358
  export function logThoughtHandler(worktreePath, data) {
316
359
  const title = data.title?.trim();
@@ -319,7 +362,7 @@ export function logThoughtHandler(worktreePath, data) {
319
362
  const content = data.content?.trim();
320
363
  if (!content)
321
364
  throw new Error('content is required');
322
- const thoughtsDir = path.join(worktreePath, '.ai', 'thoughts');
365
+ const thoughtsDir = path.join(worktreePath, '.ai', 'thoughts', 'logs');
323
366
  fs.mkdirSync(thoughtsDir, { recursive: true });
324
367
  const date = new Date().toISOString().slice(0, 10);
325
368
  const slug = title
@@ -332,7 +375,7 @@ export function logThoughtHandler(worktreePath, data) {
332
375
  const tagSuffix = data.tag ? `-${data.tag.replace(/[^a-z0-9]+/gi, '-').toLowerCase()}` : '';
333
376
  const filename = `${date}-${slug}${tagSuffix}.md`;
334
377
  const abs = path.join(thoughtsDir, filename);
335
- const relPath = `.ai/thoughts/${filename}`;
378
+ const relPath = `.ai/thoughts/logs/${filename}`;
336
379
  const header = `# ${title}\n\n_${new Date().toISOString()}_${data.tag ? ` · tag: \`${data.tag}\`` : ''}\n\n`;
337
380
  fs.writeFileSync(abs, header + content + (content.endsWith('\n') ? '' : '\n'), 'utf-8');
338
381
  return { path: relPath };
@@ -1,12 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import fs from 'node:fs';
3
- import path from 'node:path';
4
2
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
5
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
6
4
  import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
7
5
  import { getDb } from '../server/db/index.js';
8
6
  import { runMigrations } from '../server/db/migrations.js';
9
- import { createTaskHandler, cronListHandler, deleteTaskHandler, getDevServerStatusHandler, getSessionUsageHandler, getSettingsHandler, getWorkspaceInfoHandler, listDocumentsHandler, listTasksHandler, listWorkspaceImagesHandler, logThoughtHandler, markAutoLoopReadyHandler, markTaskDoneHandler, readDocumentHandler, setWorkspaceAgentDescriptionHandler, updateTaskHandler, } from './kobo-tasks-handlers.js';
7
+ import { createTaskHandler, cronListHandler, deleteTaskHandler, getDevServerStatusHandler, getSessionUsageHandler, getSettingsHandler, getTicketSourcesHandler, getWorkspaceInfoHandler, listDocumentsHandler, listTasksHandler, listWorkspaceImagesHandler, logThoughtHandler, markAutoLoopReadyHandler, markTaskDoneHandler, readDocumentHandler, setWorkspaceAgentDescriptionHandler, updateTaskHandler, } from './kobo-tasks-handlers.js';
10
8
  const workspaceId = process.env.KOBO_WORKSPACE_ID;
11
9
  const dbPath = process.env.KOBO_DB_PATH;
12
10
  const settingsPath = process.env.KOBO_SETTINGS_PATH;
@@ -283,8 +281,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
283
281
  annotations: { destructiveHint: false, openWorldHint: false },
284
282
  },
285
283
  {
286
- name: 'get_notion_ticket',
287
- description: 'CALL when the user references "the ticket", "the Notion page", or when you need the source-of-truth text for the mission. Returns the Notion URL + locally-extracted ticket content from .ai/thoughts/.',
284
+ name: 'get_ticket',
285
+ description: 'CALL when the user references "the ticket", "the issue", "the Notion page", or when you need the source-of-truth text for the mission. Works for any source a Notion ticket or a Sentry issue. Returns `{ sources: [{ type, url, content }] }`, one entry per imported ticket (type is "notion" or "sentry"). Usually one source; empty when none was imported.',
288
286
  inputSchema: { type: 'object', properties: {}, required: [] },
289
287
  annotations: { readOnlyHint: true, openWorldHint: false },
290
288
  },
@@ -363,7 +361,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
363
361
  },
364
362
  {
365
363
  name: 'log_thought',
366
- description: 'CALL WHEN you make a decision worth remembering — architecture choice, trade-off taken, dead-end avoided, pattern discovered. Appends a dated markdown file to .ai/thoughts/. Keep entries short and focused; one decision per call. Use create_task for actionable follow-ups instead.',
364
+ description: 'CALL WHEN you make a decision worth remembering — architecture choice, trade-off taken, dead-end avoided, pattern discovered. Appends a dated markdown file to .ai/thoughts/logs/. Keep entries short and focused; one decision per call. Use create_task for actionable follow-ups instead.',
367
365
  inputSchema: {
368
366
  type: 'object',
369
367
  properties: {
@@ -589,21 +587,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
589
587
  const info = getWorkspaceInfoHandler(db, workspaceId);
590
588
  return ok(listWorkspaceImagesHandler(info.worktreePath));
591
589
  }
592
- if (name === 'get_notion_ticket') {
590
+ // `get_notion_ticket` is the pre-1.7 name, kept as a back-compat alias so
591
+ // sessions resumed against an older tool list still resolve.
592
+ if (name === 'get_ticket' || name === 'get_notion_ticket') {
593
593
  const info = getWorkspaceInfoHandler(db, workspaceId);
594
- const thoughtsDir = path.join(info.worktreePath, '.ai', 'thoughts');
595
- let ticketContent = '';
596
- if (fs.existsSync(thoughtsDir)) {
597
- const files = fs.readdirSync(thoughtsDir).filter((f) => f.endsWith('.md'));
598
- for (const file of files) {
599
- ticketContent += `${fs.readFileSync(path.join(thoughtsDir, file), 'utf-8')}\n`;
600
- }
601
- }
602
- return ok({
603
- notionUrl: info.notionUrl,
604
- notionPageId: info.notionPageId,
605
- ticketContent: ticketContent.trim() || null,
606
- });
594
+ return ok({ sources: getTicketSourcesHandler(info.worktreePath) });
607
595
  }
608
596
  if (name === 'get_git_info') {
609
597
  const result = await backendRequest('GET', `/api/workspaces/${workspaceId}/git-stats`);
@@ -281,6 +281,25 @@ export const migrations = [
281
281
  }
282
282
  },
283
283
  },
284
+ {
285
+ version: 24,
286
+ name: 'add-workspace-chat-history-table',
287
+ migrate: (db) => {
288
+ // Per-workspace chat history (user-typed messages). CASCADE on workspace
289
+ // delete. Index supports the typical "latest N for a workspace" query via
290
+ // (workspace_id, id DESC).
291
+ db.exec(`
292
+ CREATE TABLE IF NOT EXISTS workspace_chat_history (
293
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
294
+ workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
295
+ message TEXT NOT NULL,
296
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
297
+ );
298
+ CREATE INDEX IF NOT EXISTS idx_workspace_chat_history_workspace_id_id
299
+ ON workspace_chat_history(workspace_id, id DESC);
300
+ `);
301
+ },
302
+ },
284
303
  ];
285
304
  /** Current schema version — always equals the highest migration version. */
286
305
  export const SCHEMA_VERSION = migrations.length > 0 ? migrations[migrations.length - 1].version : 1;
@@ -106,8 +106,17 @@ export function initSchema(db) {
106
106
  fetched_at TEXT NOT NULL
107
107
  );
108
108
 
109
+ CREATE TABLE IF NOT EXISTS workspace_chat_history (
110
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
111
+ workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
112
+ message TEXT NOT NULL,
113
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
114
+ );
115
+
109
116
  CREATE INDEX IF NOT EXISTS idx_tasks_workspace_id ON tasks(workspace_id);
110
117
  CREATE INDEX IF NOT EXISTS idx_agent_sessions_workspace_id ON agent_sessions(workspace_id);
111
118
  CREATE INDEX IF NOT EXISTS idx_ws_events_workspace_id ON ws_events(workspace_id);
119
+ CREATE INDEX IF NOT EXISTS idx_workspace_chat_history_workspace_id_id
120
+ ON workspace_chat_history(workspace_id, id DESC);
112
121
  `);
113
122
  }
@@ -1,6 +1,7 @@
1
1
  import { Hono } from 'hono';
2
2
  import { DEFAULT_NOTION_INITIAL_PROMPT, DEFAULT_SENTRY_INITIAL_PROMPT, } from '../services/initial-prompt-template-service.js';
3
3
  import { DEFAULT_REVIEW_PROMPT_TEMPLATE } from '../services/review-template-service.js';
4
+ import { DEFAULT_CHANGE_SOURCE_BRANCH_SCRIPT } from '../services/settings-defaults.js';
4
5
  import * as settingsService from '../services/settings-service.js';
5
6
  import { DEFAULT_GIT_CONVENTIONS, DEFAULT_PR_PROMPT_TEMPLATE, } from '../services/settings-service.js';
6
7
  import { listTemplates, replaceAllTemplates } from '../services/templates-service.js';
@@ -39,6 +40,7 @@ app.get('/defaults', (c) => {
39
40
  gitConventions: DEFAULT_GIT_CONVENTIONS,
40
41
  notionInitialPromptTemplate: DEFAULT_NOTION_INITIAL_PROMPT,
41
42
  sentryInitialPromptTemplate: DEFAULT_SENTRY_INITIAL_PROMPT,
43
+ changeSourceBranchScript: DEFAULT_CHANGE_SOURCE_BRANCH_SCRIPT,
42
44
  });
43
45
  });
44
46
  // GET /api/settings/mcp-servers — list active MCP servers from Claude config