@loicngr/kobo 1.11.43 → 1.11.44

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 (144) hide show
  1. package/AGENTS.md +36 -1
  2. package/CHANGELOG.md +6 -0
  3. package/dist/mcp-server/kobo-tasks-handlers.js +31 -74
  4. package/dist/mcp-server/kobo-tasks-server.js +51 -4
  5. package/dist/server/db/auto-loop-schema.js +44 -0
  6. package/dist/server/db/migrations.js +4 -0
  7. package/dist/server/db/review-return-schema.js +10 -0
  8. package/dist/server/db/schema.js +4 -0
  9. package/dist/server/index.js +10 -9
  10. package/dist/server/routes/auto-loop-messages.js +51 -0
  11. package/dist/server/routes/workspaces.js +46 -136
  12. package/dist/server/services/agent/engines/claude-code/engine.js +1 -0
  13. package/dist/server/services/agent/engines/codex/engine.js +91 -31
  14. package/dist/server/services/agent/orchestrator.js +237 -88
  15. package/dist/server/services/agent/session-controller.js +42 -2
  16. package/dist/server/services/auto-loop-message-service.js +79 -0
  17. package/dist/server/services/auto-loop-service.js +225 -86
  18. package/dist/server/services/auto-loop-state-service.js +30 -0
  19. package/dist/server/services/cron-service.js +30 -16
  20. package/dist/server/services/quota-backoff-service.js +42 -10
  21. package/dist/server/services/review-return-service.js +64 -0
  22. package/dist/server/services/review-service.js +187 -0
  23. package/dist/server/services/settings-service.js +3 -3
  24. package/dist/server/services/task-mutations.js +186 -0
  25. package/dist/server/services/wakeup-service.js +16 -1
  26. package/dist/server/services/workspace-message-service.js +16 -2
  27. package/dist/server/services/workspace-service.js +25 -36
  28. package/dist/shared/auto-loop-prompts.js +22 -11
  29. package/dist/shared/auto-loop-types.js +1 -0
  30. package/dist/shared/review.js +6 -0
  31. package/dist/shared/task-verification.js +11 -0
  32. package/package.json +12 -12
  33. package/src/client/dist/pwa/assets/{ActivityFeed-D1e-zV3O.js → ActivityFeed-D_y4aikp.js} +1 -1
  34. package/src/client/dist/pwa/assets/{ChangelogPage-DX1WGFea.js → ChangelogPage-DSKCvpr2.js} +1 -1
  35. package/src/client/dist/pwa/assets/{ClosePopup-BqW5dy-O.js → ClosePopup-CWjIRHxu.js} +1 -1
  36. package/src/client/dist/pwa/assets/{ComparisonPanel-B4ne55Xj.js → ComparisonPanel-DDxcsxyC.js} +1 -1
  37. package/src/client/dist/pwa/assets/{CreatePage-Bx_GpUni.js → CreatePage-DKVEknbL.js} +2 -2
  38. package/src/client/dist/pwa/assets/{DashboardPage-Cv6HJEcO.js → DashboardPage-Dg2s9nnv.js} +1 -1
  39. package/src/client/dist/pwa/assets/{DiffViewer-COwGxPt6.js → DiffViewer-BSzNScIr.js} +4 -4
  40. package/src/client/dist/pwa/assets/{DocumentsPanel-DCXpLLb7.js → DocumentsPanel-DfEoI_f7.js} +1 -1
  41. package/src/client/dist/pwa/assets/{DrawerToggleButton-DRL3RCkM.js → DrawerToggleButton-D85PwhKb.js} +1 -1
  42. package/src/client/dist/pwa/assets/GitPanel-CuG8MlgF.js +3 -0
  43. package/src/client/dist/pwa/assets/{HealthPage-XrOuMoDJ.js → HealthPage-CaLCpejR.js} +1 -1
  44. package/src/client/dist/pwa/assets/MainLayout-CDxYIJ_s.js +2 -0
  45. package/src/client/dist/pwa/assets/MainLayout-DTLNunOi.css +1 -0
  46. package/src/client/dist/pwa/assets/{QBadge-DbkxGtht.js → QBadge-FWC1zDCq.js} +1 -1
  47. package/src/client/dist/pwa/assets/{QBanner-DhqH95GW.js → QBanner-CcRI_QIy.js} +1 -1
  48. package/src/client/dist/pwa/assets/QBtn-DEdx_Y1V.js +1 -0
  49. package/src/client/dist/pwa/assets/{QBtnDropdown-163oM6T_.js → QBtnDropdown-qJWGcZpC.js} +1 -1
  50. package/src/client/dist/pwa/assets/{QBtnGroup-DELKYo3u.js → QBtnGroup-CAoD6G0Q.js} +1 -1
  51. package/src/client/dist/pwa/assets/{QBtnToggle-BI3k3Ch2.js → QBtnToggle-C_hQ2UCQ.js} +1 -1
  52. package/src/client/dist/pwa/assets/{QChip-owciRuZu.js → QChip-DCPUhqXX.js} +1 -1
  53. package/src/client/dist/pwa/assets/QDrawer-DX3PhMqC.js +1 -0
  54. package/src/client/dist/pwa/assets/{QItemSection-D_5_0UrX.js → QItemSection-BhqphaH6.js} +1 -1
  55. package/src/client/dist/pwa/assets/{QList-qupzuEqO.js → QList-CHm4JNP0.js} +1 -1
  56. package/src/client/dist/pwa/assets/{QMarkupTable-B9QuKtK_.js → QMarkupTable-CNpiA9oq.js} +1 -1
  57. package/src/client/dist/pwa/assets/{QPage-DaX9sMQC.js → QPage-BXBw2Alx.js} +1 -1
  58. package/src/client/dist/pwa/assets/QSelect-CY4TWW_j.js +1 -0
  59. package/src/client/dist/pwa/assets/{QSpinnerDots-DnCV3VzI.js → QSpinnerDots-BkFiCsH3.js} +1 -1
  60. package/src/client/dist/pwa/assets/{QTooltip-BQWY7aeM.js → QTooltip-cDK5AOQ5.js} +1 -1
  61. package/src/client/dist/pwa/assets/{SchedulePanel-Bos94OyR.js → SchedulePanel-LvHiZ6ZH.js} +1 -1
  62. package/src/client/dist/pwa/assets/{SearchPage-DOHkP5lj.js → SearchPage-BO-J4aNc.js} +1 -1
  63. package/src/client/dist/pwa/assets/{SettingsPage-DtGsd0XH.js → SettingsPage-Aw89fRSq.js} +2 -2
  64. package/src/client/dist/pwa/assets/{SplitWorkspacePage-DL4kp9Ah.js → SplitWorkspacePage-BIEceNwH.js} +1 -1
  65. package/src/client/dist/pwa/assets/{TerminalPanel-DHDnyiYh.js → TerminalPanel-DitOCad1.js} +1 -1
  66. package/src/client/dist/pwa/assets/{TouchPan-BnHXDR6F.js → TouchPan-DvPAhZsa.js} +1 -1
  67. package/src/client/dist/pwa/assets/{TourReplayButton-ClVcB6mO.js → TourReplayButton-CJYdH3Jf.js} +1 -1
  68. package/src/client/dist/pwa/assets/{WorkspaceAttentionLabels-Bxe6oIJI.js → WorkspaceAttentionLabels-CY-9z9nw.js} +1 -1
  69. package/src/client/dist/pwa/assets/WorkspacePage-DAVo-E4p.css +1 -0
  70. package/src/client/dist/pwa/assets/WorkspacePage-xH_OyA3r.js +4 -0
  71. package/src/client/dist/pwa/assets/{browser-Bkx4fbxl.js → browser-BsB6cFv4.js} +1 -1
  72. package/src/client/dist/pwa/assets/build-path-tree-CW-ER1Mt.js +1 -0
  73. package/src/client/dist/pwa/assets/{de-BU0dILBs.js → de-Mu7biceO.js} +2 -2
  74. package/src/client/dist/pwa/assets/documents-SCd3w8xV.js +1 -0
  75. package/src/client/dist/pwa/assets/{editor.api-DreEG0If.js → editor.api-BUdM7I6y.js} +1 -1
  76. package/src/client/dist/pwa/assets/editor.worker-iO5iN0DB.js +33 -0
  77. package/src/client/dist/pwa/assets/efforts-DTJQP9or.js +9 -0
  78. package/src/client/dist/pwa/assets/engineFeatures-B-9jHocY.css +1 -0
  79. package/src/client/dist/pwa/assets/engineFeatures-Bj6Ob41d.js +1 -0
  80. package/src/client/dist/pwa/assets/{es-CNp0s9na.js → es-BY5IfNI2.js} +2 -2
  81. package/src/client/dist/pwa/assets/expand-template-SM8vvYyO.js +1 -0
  82. package/src/client/dist/pwa/assets/{format-B7z3F-Mw.js → format-BjQxf4pe.js} +3 -3
  83. package/src/client/dist/pwa/assets/{formatters-BTBiU0v3.js → formatters-BejhmNTV.js} +1 -1
  84. package/src/client/dist/pwa/assets/{fr-RqJyWSdm.js → fr-BMXCzFCM.js} +2 -2
  85. package/src/client/dist/pwa/assets/{freemarker2-BZNTVQ3K.js → freemarker2-5d5GhyRp.js} +1 -1
  86. package/src/client/dist/pwa/assets/{handlebars-k8tN0dI3.js → handlebars-DaafEsDK.js} +1 -1
  87. package/src/client/dist/pwa/assets/{html-iZL3bbXq.js → html-CwHBDd2A.js} +1 -1
  88. package/src/client/dist/pwa/assets/i18n-CgWW5vfb.js +1 -0
  89. package/src/client/dist/pwa/assets/index-0wtBh1Xt.css +1 -0
  90. package/src/client/dist/pwa/assets/index-Di9Sz5E8.js +20 -0
  91. package/src/client/dist/pwa/assets/{it-DSxNuCwL.js → it-ByPZQK_K.js} +2 -2
  92. package/src/client/dist/pwa/assets/{javascript-TV6gg0MO.js → javascript-DSNIEEH5.js} +1 -1
  93. package/src/client/dist/pwa/assets/{liquid-Cifa5ujF.js → liquid-Dma1UpA1.js} +1 -1
  94. package/src/client/dist/pwa/assets/{mdx-Bnj_8yGE.js → mdx-D8rqEAws.js} +1 -1
  95. package/src/client/dist/pwa/assets/{monaco-contributions-pEh0n9Wk.js → monaco-contributions-2E7O2zoi.js} +1 -1
  96. package/src/client/dist/pwa/assets/{monaco.contribution-D7NmJUJH.js → monaco.contribution-DqOgo72G.js} +2 -2
  97. package/src/client/dist/pwa/assets/{network-auth-3_5p4fdc.js → network-auth-DNmhJTZn.js} +1 -1
  98. package/src/client/dist/pwa/assets/{notify-theme-D_nf9AeF.js → notify-theme-B6Hiwvwb.js} +1 -1
  99. package/src/client/dist/pwa/assets/{pending-diff-open-CUnxC1xw.js → pending-diff-open-CeiVIFZn.js} +1 -1
  100. package/src/client/dist/pwa/assets/permissionModes-DWdtf81n.js +36 -0
  101. package/src/client/dist/pwa/assets/{pinia-BKqmoVru.js → pinia-CTJ3OfP3.js} +1 -1
  102. package/src/client/dist/pwa/assets/{private.use-form-C7umbGO-.js → private.use-form-Bk8ti264.js} +1 -1
  103. package/src/client/dist/pwa/assets/{project-color-CihwpDAA.js → project-color-BCp9SWoI.js} +1 -1
  104. package/src/client/dist/pwa/assets/{python-DOoNBSNL.js → python-f-9NqRlI.js} +1 -1
  105. package/src/client/dist/pwa/assets/{razor-Bvq5vVpJ.js → razor-DsCmoX0h.js} +1 -1
  106. package/src/client/dist/pwa/assets/reactivity.esm-bundler-Bo9anJjg.js +1 -0
  107. package/src/client/dist/pwa/assets/render-chat-markdown-DK5BD9mU.js +67 -0
  108. package/src/client/dist/pwa/assets/{runtime-core.esm-bundler-CDnVUfeZ.js → runtime-core.esm-bundler-CNFKm4ij.js} +1 -1
  109. package/src/client/dist/pwa/assets/{typescript-VVzxoeyM.js → typescript-cEHg4L10.js} +1 -1
  110. package/src/client/dist/pwa/assets/{use-checkbox-FnauAgTp.js → use-checkbox-CnvHBhSK.js} +1 -1
  111. package/src/client/dist/pwa/assets/{use-id-BIuaWKkB.js → use-id-wXd4c8FA.js} +1 -1
  112. package/src/client/dist/pwa/assets/use-is-mobile-DpKI3XMy.js +1 -0
  113. package/src/client/dist/pwa/assets/{use-search-index-status-DtLzygcy.js → use-search-index-status-Bluf9L99.js} +1 -1
  114. package/src/client/dist/pwa/assets/use-tours-BmTbosmB.js +2 -0
  115. package/src/client/dist/pwa/assets/{workspace-templates-1ui80Kqo.js → workspace-templates-DUWDLMIX.js} +1 -1
  116. package/src/client/dist/pwa/assets/{xml-CzGFIxQa.js → xml-Plt-Y7we.js} +1 -1
  117. package/src/client/dist/pwa/assets/{yaml-Bl7nDJeC.js → yaml-ScEmaAeN.js} +1 -1
  118. package/src/client/dist/pwa/index.html +10 -10
  119. package/src/client/dist/pwa/sw.js +1 -1
  120. package/src/mcp-server/kobo-tasks-handlers.ts +68 -104
  121. package/src/mcp-server/kobo-tasks-server.ts +56 -4
  122. package/src/client/dist/pwa/assets/GitPanel-C9mz4GXF.js +0 -3
  123. package/src/client/dist/pwa/assets/MainLayout-BwV89R-l.css +0 -1
  124. package/src/client/dist/pwa/assets/MainLayout-CwWavQjV.js +0 -2
  125. package/src/client/dist/pwa/assets/QBtn-CZaBclNw.js +0 -1
  126. package/src/client/dist/pwa/assets/QDrawer-DNEj8wLZ.js +0 -1
  127. package/src/client/dist/pwa/assets/QSelect-BUlOQe5k.js +0 -1
  128. package/src/client/dist/pwa/assets/WorkspacePage-Clc4Fl3B.js +0 -4
  129. package/src/client/dist/pwa/assets/WorkspacePage-DCg9zSxz.css +0 -1
  130. package/src/client/dist/pwa/assets/build-path-tree-BpSY7N5O.js +0 -1
  131. package/src/client/dist/pwa/assets/documents-BIlWh3o-.js +0 -1
  132. package/src/client/dist/pwa/assets/editor.worker-CwQAd_xr.js +0 -30
  133. package/src/client/dist/pwa/assets/efforts-D_yTzymv.js +0 -9
  134. package/src/client/dist/pwa/assets/engineFeatures-C33LTVkK.js +0 -1
  135. package/src/client/dist/pwa/assets/expand-template-wRhAXT-1.js +0 -1
  136. package/src/client/dist/pwa/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNa-Dr0goTwe.woff +0 -0
  137. package/src/client/dist/pwa/assets/i18n--PCet00a.js +0 -1
  138. package/src/client/dist/pwa/assets/index-CPfrY38K.js +0 -20
  139. package/src/client/dist/pwa/assets/index-CUoUy6tH.css +0 -1
  140. package/src/client/dist/pwa/assets/permissionModes-DC51Pz84.js +0 -36
  141. package/src/client/dist/pwa/assets/reactivity.esm-bundler-DEJj9cFv.js +0 -1
  142. package/src/client/dist/pwa/assets/render-chat-markdown-BQ_saiy5.js +0 -67
  143. package/src/client/dist/pwa/assets/use-is-mobile-CWjqe8gC.js +0 -1
  144. package/src/client/dist/pwa/assets/use-tours-BKeYrJaQ.js +0 -2
package/AGENTS.md CHANGED
@@ -116,6 +116,7 @@ src/
116
116
  | `tasks` | workspace sub-items: title, status, `is_acceptance_criterion`, sort_order; CASCADE DELETE on workspace |
117
117
  | `agent_sessions` | agent-engine sessions: pid where applicable, engine session id, status, timestamps, model, and name |
118
118
  | `ws_events` | persisted WebSocket events for replay on reconnect: type, payload, session_id, created_at |
119
+ | `pending_review_returns` | one-shot return from a review to its original session and LLM configuration (migration v44) |
119
120
  | `pending_wakeups` | one-row-per-workspace scheduler for the `schedule_wakeup` MCP tool: target_at (ISO UTC), prompt, reason; CASCADE DELETE on workspace |
120
121
  | `pending_quota_backoffs` | persisted quota retries and their attempt counts |
121
122
  | `pending_crons` | recurring workspace schedules and their next run |
@@ -135,7 +136,11 @@ src/
135
136
 
136
137
  `worktree_purged_at` + `worktree_purge_restore_data` drive the disk-space purge feature (see [Worktree purge](#worktree-purge) below): when set, the workspace's worktree folder has been removed from disk but the chat history is preserved. `worktree_purge_restore_data` is a JSON blob (`{ prNumber, prUrl, forge, mergeCommitSha, originalWorktreePath, originalSourceBranch, originalWorkingBranch }`) captured at purge time for future "Restore" UX. Both fields are cleared automatically by the pr-watcher when the worktree folder reappears on disk.
137
138
 
138
- `auto_loop` (bool, default 0), `auto_loop_ready` (bool, default 0) and `no_progress_streak` (int, default 0) drive the auto-loop feature: when `auto_loop=1`, `session:ended` triggers `auto-loop-service.onSessionEnded` which either spawns the next iteration via a fresh `startAgent(resume=false)`, disables with `reason='completed'` (no pending tasks), or disables with `reason='stall'` (3 consecutive sessions without a task completed). Archive + delete both auto-disable.
139
+ `auto_loop` preserves the user's execution intent. `auto_loop_runs` (migration v45) stores phase (`grooming | execution | finalization`), operational state (`active | waiting | blocked | completed | stopped`), iteration count and diagnostic recovery. `auto_loop_progress` records monotone task milestones; metadata changes are not progress. After three stagnant iterations, run one diagnostic and two further attempts, then block explicitly without clearing intent. Quotas wait for their actual reset, including multi-day windows; only transient failures use the retry cap. Archived/deleted workspaces disable the loop.
140
+
141
+ Tasks have `role` (`work | finalization`) and structured `verification`. MCP and HTTP share transactional task mutations. An auto-loop task cannot become done without passing reported checks. Finalization runs after all other tasks/criteria, is invalidated by scope changes, and is mandatory before completion. The proof is agent-reported evidence, not server-side execution of arbitrary commands.
142
+
143
+ `auto_loop_messages` durably queues next-iteration instructions, independently of engine session ids. A dispatch interrupted before confirmed completion becomes `unknown`, requiring explicit acknowledgement or retry. Unknown or in-flight instructions prevent completion. Actual engine closure (`EngineProcess.closed`), not a logical end event alone, releases controller ownership and permits the next writer.
139
144
 
140
145
  `brainstorm_model` (nullable, set only at creation) lets a workspace created with `auto_loop=1` run its initial brainstorming session on a different model than the auto-loop iterations that follow, while staying on the same engine. `POST /api/workspaces` uses it for the very first `startAgent` call only; every subsequent auto-loop iteration (`auto-loop-service.ts`'s `spawnNextIteration`) always reads `model`, unchanged. The switch to `model` takes effect once that first live session ends naturally — not at the `[BRAINSTORM_COMPLETE]` marker, since the same session keeps running under `brainstorm_model` until it stops on its own.
141
146
 
@@ -259,6 +264,36 @@ Background: the engine was migrated from `@openai/codex-sdk` (one-shot `codex ex
259
264
 
260
265
  ## Workspace operations
261
266
 
267
+ ### Review LLM and return to the original session
268
+
269
+ The current conversation is the running session, or the last used session by
270
+ `max(startedAt, endedAt)` after it stops. Keep the session list in creation order,
271
+ but do not use its first entry to identify the current conversation: a temporary
272
+ review can return to an older session. The stale-session banner and the next
273
+ review's return target must recognize that original session, including on reload.
274
+
275
+ `StartReviewDialog.vue` uses the shared model/effort/permission catalogues. Changed
276
+ settings force a new session on both client and server. `review-service.ts`
277
+ validates before side effects, waits for confirmed shutdown, then applies the
278
+ selected configuration and launches the review. Without a return request, these
279
+ settings remain the workspace defaults. Legacy requests retain their behavior.
280
+
281
+ The optional `returnToSession` flag captures the exact resumable original session.
282
+ `pending_review_returns` persists its configuration and the review session id
283
+ before switching engines. The orchestrator intercepts a successful review end
284
+ (including watchdog closure without a nonzero exit), restores configuration and
285
+ resumes that exact native conversation with the reviewer's final text report.
286
+ Resume selection is scoped to the target engine; never pass a reviewer's native
287
+ conversation id to the original engine, even after cancellation or restart.
288
+ The report reassembles text deltas/final snapshots, is bounded to 24,000 characters,
289
+ and links back to the complete review session through the conversation tool.
290
+ The receiving agent is asked to summarize and await instructions before fixes.
291
+ The intent is consumed before delivery, preventing repeated ends from sending twice.
292
+ Errors restore settings without automatically resuming; explicit stop cancels the
293
+ return. Startup restores interrupted-review settings without replaying a possibly
294
+ already-delivered handoff. Preserve these guards and the auto-loop interception.
295
+ `workspace:configuration` is ephemeral and updates all four LLM settings across tabs.
296
+
262
297
  ### Delete confirmation
263
298
 
264
299
  The workspace deletion dialog requires the exact `workingBranch`, not the editable
package/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@ 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.44
8
+
9
+ - chore(deps): update packages and node to 24.21.0
10
+ - fix(auto-loop): preserve mission intent until verified completion
11
+ - feat(review): configure review LLM and return to original session
12
+
7
13
  ## 1.11.43
8
14
 
9
15
  - fix(workspaces): refine watchdog stops and deletion confirmation
@@ -3,10 +3,12 @@ import path from 'node:path';
3
3
  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
+ import { createTaskRecord, deleteTaskRecord, updateTaskRecord, } from '../server/services/task-mutations.js';
6
7
  import { slugifyProjectName } from '../server/utils/project-slug.js';
7
8
  import { ensureDirectoryInside, resolveExistingPathInside } from '../server/utils/safe-path.js';
8
9
  import { resolveWorkspaceWorktreePath } from '../server/utils/worktree-paths.js';
9
10
  import { MASKED_SECRET, SECRET_GLOBAL_KEYS } from '../shared/consts.js';
11
+ import { parseTaskVerification } from '../shared/task-verification.js';
10
12
  /** Allowed task status values. */
11
13
  export const VALID_TASK_STATUSES = ['pending', 'in_progress', 'done'];
12
14
  function rowToDto(row) {
@@ -15,12 +17,15 @@ function rowToDto(row) {
15
17
  title: row.title,
16
18
  status: row.status,
17
19
  is_acceptance_criterion: row.is_acceptance_criterion === 1,
20
+ sort_order: row.sort_order,
21
+ role: row.role,
22
+ verification: parseTaskVerification(row.verification),
18
23
  };
19
24
  }
20
25
  /** Return all tasks for a workspace, ordered by sort_order. */
21
26
  export function listTasksHandler(db, workspaceId) {
22
27
  const rows = db
23
- .prepare('SELECT id, title, status, is_acceptance_criterion FROM tasks WHERE workspace_id = ? ORDER BY sort_order ASC')
28
+ .prepare('SELECT * FROM tasks WHERE workspace_id = ? ORDER BY sort_order ASC, rowid ASC')
24
29
  .all(workspaceId);
25
30
  return rows.map(rowToDto);
26
31
  }
@@ -81,86 +86,38 @@ export function setWorkspaceAgentDescriptionHandler(db, workspaceId, args) {
81
86
  }
82
87
  return { ok: true, description: stored };
83
88
  }
84
- /** Set a task's status to "done" and return the updated task. */
85
- export function markTaskDoneHandler(db, workspaceId, taskId) {
86
- const now = new Date().toISOString();
87
- const result = db
88
- .prepare('UPDATE tasks SET status = ?, updated_at = ? WHERE id = ? AND workspace_id = ?')
89
- .run('done', now, taskId, workspaceId);
90
- if (result.changes === 0) {
91
- throw new Error(`Task '${taskId}' not found in workspace '${workspaceId}'`);
92
- }
93
- const row = db
94
- .prepare('SELECT id, title, status, is_acceptance_criterion FROM tasks WHERE id = ?')
95
- .get(taskId);
89
+ /** Set a task's status to done; auto-loop tasks require successful evidence. */
90
+ export function markTaskDoneHandler(db, workspaceId, taskId, verification) {
91
+ const row = updateTaskRecord(db, workspaceId, taskId, {
92
+ status: 'done',
93
+ ...(verification !== undefined ? { verification } : {}),
94
+ });
96
95
  return { success: true, task: rowToDto(row) };
97
96
  }
98
- /** Create a new task appended at the end of the workspace's task list. */
97
+ /** Create a task at an explicit position or append it to the list. */
99
98
  export function createTaskHandler(db, workspaceId, data) {
100
- if (!data.title?.trim()) {
101
- throw new Error('title is required');
102
- }
103
- // Verify workspace exists
104
- const ws = db.prepare('SELECT id FROM workspaces WHERE id = ?').get(workspaceId);
105
- if (!ws) {
106
- throw new Error(`Workspace '${workspaceId}' not found`);
107
- }
108
- const id = nanoid();
109
- const now = new Date().toISOString();
110
- const isAC = data.is_acceptance_criterion ? 1 : 0;
111
- // Append at the end: max(sort_order) + 1
112
- const maxRow = db
113
- .prepare('SELECT COALESCE(MAX(sort_order), -1) AS max FROM tasks WHERE workspace_id = ?')
114
- .get(workspaceId);
115
- const sortOrder = maxRow.max + 1;
116
- db.prepare('INSERT INTO tasks (id, workspace_id, title, status, is_acceptance_criterion, sort_order, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)').run(id, workspaceId, data.title.trim(), 'pending', isAC, sortOrder, now, now);
117
- const row = db.prepare('SELECT id, title, status, is_acceptance_criterion FROM tasks WHERE id = ?').get(id);
118
- return rowToDto(row);
99
+ return rowToDto(createTaskRecord(db, workspaceId, {
100
+ title: data.title,
101
+ isAcceptanceCriterion: data.is_acceptance_criterion,
102
+ sortOrder: data.sort_order,
103
+ afterTaskId: data.after_task_id,
104
+ role: data.role,
105
+ }));
119
106
  }
120
- /** Update one or more fields of an existing task (title, status, or acceptance criterion flag). */
107
+ /** Update task fields atomically, including insertion order and verification. */
121
108
  export function updateTaskHandler(db, workspaceId, taskId, data) {
122
- // Verify task belongs to workspace
123
- const existing = db.prepare('SELECT id FROM tasks WHERE id = ? AND workspace_id = ?').get(taskId, workspaceId);
124
- if (!existing) {
125
- throw new Error(`Task '${taskId}' not found in workspace '${workspaceId}'`);
126
- }
127
- const sets = [];
128
- const values = [];
129
- if (data.title !== undefined) {
130
- if (!data.title.trim())
131
- throw new Error('title cannot be empty');
132
- sets.push('title = ?');
133
- values.push(data.title.trim());
134
- }
135
- if (data.status !== undefined) {
136
- if (!VALID_TASK_STATUSES.includes(data.status)) {
137
- throw new Error(`Invalid status '${data.status}'. Must be one of: ${VALID_TASK_STATUSES.join(', ')}`);
138
- }
139
- sets.push('status = ?');
140
- values.push(data.status);
141
- }
142
- if (data.is_acceptance_criterion !== undefined) {
143
- sets.push('is_acceptance_criterion = ?');
144
- values.push(data.is_acceptance_criterion ? 1 : 0);
145
- }
146
- if (sets.length === 0) {
147
- throw new Error('No fields to update (provide title, status, or is_acceptance_criterion)');
148
- }
149
- sets.push('updated_at = ?');
150
- values.push(new Date().toISOString());
151
- values.push(taskId);
152
- db.prepare(`UPDATE tasks SET ${sets.join(', ')} WHERE id = ?`).run(...values);
153
- const row = db
154
- .prepare('SELECT id, title, status, is_acceptance_criterion FROM tasks WHERE id = ?')
155
- .get(taskId);
156
- return rowToDto(row);
109
+ return rowToDto(updateTaskRecord(db, workspaceId, taskId, {
110
+ title: data.title,
111
+ status: data.status,
112
+ isAcceptanceCriterion: data.is_acceptance_criterion,
113
+ sortOrder: data.sort_order,
114
+ afterTaskId: data.after_task_id,
115
+ verification: data.verification,
116
+ }));
157
117
  }
158
- /** Permanently delete a task from a workspace. */
118
+ /** Permanently delete a task from a workspace and invalidate its final review. */
159
119
  export function deleteTaskHandler(db, workspaceId, taskId) {
160
- const result = db.prepare('DELETE FROM tasks WHERE id = ? AND workspace_id = ?').run(taskId, workspaceId);
161
- if (result.changes === 0) {
162
- throw new Error(`Task '${taskId}' not found in workspace '${workspaceId}'`);
163
- }
120
+ deleteTaskRecord(db, workspaceId, taskId);
164
121
  return { success: true, task_id: taskId };
165
122
  }
166
123
  /** Read the dev-server status for a workspace directly from the database. */
@@ -113,20 +113,53 @@ async function backendRequest(method, pathname, body) {
113
113
  return text ? JSON.parse(text) : null;
114
114
  }
115
115
  const server = new Server({ name: 'kobo-tasks', version: '1.0.0' }, { capabilities: { tools: {} } });
116
+ const TASK_VERIFICATION_SCHEMA = {
117
+ type: 'object',
118
+ description: 'Required to complete an auto-loop task. Describe checks actually performed; every check must be passed for done. Record failed/not_run checks only on an unfinished task.',
119
+ properties: {
120
+ method: { type: 'string', minLength: 1 },
121
+ summary: { type: 'string', minLength: 1 },
122
+ checks: {
123
+ type: 'array',
124
+ minItems: 1,
125
+ items: {
126
+ type: 'object',
127
+ properties: {
128
+ name: { type: 'string', minLength: 1 },
129
+ status: { type: 'string', enum: ['passed', 'failed', 'not_run'] },
130
+ },
131
+ required: ['name', 'status'],
132
+ },
133
+ },
134
+ },
135
+ required: ['method', 'summary', 'checks'],
136
+ };
137
+ const TASK_ORDER_PROPERTIES = {
138
+ sort_order: {
139
+ type: 'integer',
140
+ minimum: 0,
141
+ description: 'Insert/move at this order, shifting siblings. Omit to append on creation. Mutually exclusive with after_task_id.',
142
+ },
143
+ after_task_id: {
144
+ type: 'string',
145
+ description: 'Insert/move immediately after this task in the same workspace, preserving the remaining order.',
146
+ },
147
+ };
116
148
  const WORKSPACE_SCOPED_TOOLS = [
117
149
  {
118
150
  name: 'list_tasks',
119
- description: 'CALL FIRST on any non-trivial turn to know what the user wants done and what is already completed. Returns every task and acceptance criterion for the current workspace with its id and status. Re-call periodically (before marking something done, or after the user asks for a status) to stay in sync with user-added or external updates.',
151
+ description: 'CALL FIRST on any non-trivial turn to know what the user wants done and what is already completed. Returns every task and acceptance criterion for the current workspace with its id, status, sort_order, role, and verification evidence. Re-call periodically (before marking something done, or after the user asks for a status) to stay in sync with user-added or external updates.',
120
152
  inputSchema: { type: 'object', properties: {}, required: [] },
121
153
  annotations: { readOnlyHint: true, openWorldHint: false },
122
154
  },
123
155
  {
124
156
  name: 'mark_task_done',
125
- description: 'CALL AS SOON AS a task or acceptance criterion is finished AND verified (tests pass, feature works, diff committed). Do not wait for the end of the turn — the user watches progress live and marking each item as it completes is the primary signal Kōbō uses to track you.',
157
+ description: 'CALL AS SOON AS a task or acceptance criterion is finished AND verified by checks actually performed. Auto-loop requires structured verification evidence with all checks passed. Never mark done if required checks failed or were not run. Finalization also requires all work tasks and criteria done. Do not wait for the end of the turn — the user watches progress live and marking each item as it completes is the primary signal Kōbō uses to track you.',
126
158
  inputSchema: {
127
159
  type: 'object',
128
160
  properties: {
129
161
  task_id: { type: 'string', description: 'Task id from list_tasks.' },
162
+ verification: TASK_VERIFICATION_SCHEMA,
130
163
  },
131
164
  required: ['task_id'],
132
165
  },
@@ -140,10 +173,16 @@ const WORKSPACE_SCOPED_TOOLS = [
140
173
  },
141
174
  {
142
175
  name: 'create_task',
143
- description: 'CALL WHEN you discover follow-up work that was not in the original list and needs to stick around (e.g. "refactor this helper later", "add a test for edge case"). Appends at the end of the list. Do not use it for ephemeral internal notes — prefer log_thought for those.',
176
+ description: 'CALL WHEN you discover follow-up work that was not in the original list and needs to stick around (e.g. "refactor this helper later", "add a test for edge case"). Appends by default; use after_task_id for a dependent check immediately after its parent. Use role finalization for the final verification gate. Do not use it for ephemeral internal notes — prefer log_thought for those.',
144
177
  inputSchema: {
145
178
  type: 'object',
146
179
  properties: {
180
+ ...TASK_ORDER_PROPERTIES,
181
+ role: {
182
+ type: 'string',
183
+ enum: ['work', 'finalization'],
184
+ description: 'Default work. Finalization is the final verification gate and cannot finish while work/criteria remain open.',
185
+ },
147
186
  title: { type: 'string', description: 'Short, imperative title (e.g. "Add retry to fetchUser").' },
148
187
  is_acceptance_criterion: {
149
188
  type: 'boolean',
@@ -161,6 +200,8 @@ const WORKSPACE_SCOPED_TOOLS = [
161
200
  type: 'object',
162
201
  properties: {
163
202
  task_id: { type: 'string', description: 'Task id from list_tasks.' },
203
+ ...TASK_ORDER_PROPERTIES,
204
+ verification: TASK_VERIFICATION_SCHEMA,
164
205
  title: { type: 'string', description: 'New title (optional).' },
165
206
  status: {
166
207
  type: 'string',
@@ -612,7 +653,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
612
653
  const taskId = a.task_id;
613
654
  if (!taskId)
614
655
  return fail('task_id parameter is required');
615
- const result = markTaskDoneHandler(db, workspaceId, taskId);
656
+ const result = markTaskDoneHandler(db, workspaceId, taskId, a.verification);
616
657
  void notifyBackend(taskId);
617
658
  return ok(result);
618
659
  }
@@ -627,7 +668,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
627
668
  return fail('title parameter is required');
628
669
  const task = createTaskHandler(db, workspaceId, {
629
670
  title,
671
+ role: a.role,
630
672
  is_acceptance_criterion: a.is_acceptance_criterion,
673
+ sort_order: a.sort_order,
674
+ after_task_id: a.after_task_id,
631
675
  });
632
676
  void notifyTasksUpdated();
633
677
  return ok(task);
@@ -639,7 +683,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
639
683
  const task = updateTaskHandler(db, workspaceId, taskId, {
640
684
  title: a.title,
641
685
  status: a.status,
686
+ verification: a.verification,
642
687
  is_acceptance_criterion: a.is_acceptance_criterion,
688
+ sort_order: a.sort_order,
689
+ after_task_id: a.after_task_id,
643
690
  });
644
691
  void notifyTasksUpdated();
645
692
  return ok(task);
@@ -0,0 +1,44 @@
1
+ /** Shared by fresh installs and the append-only upgrade. */
2
+ export function initAutoLoopSchema(db) {
3
+ const columns = db.prepare('PRAGMA table_info(tasks)').all();
4
+ if (columns.length) {
5
+ if (!columns.some((c) => c.name === 'role'))
6
+ db.exec("ALTER TABLE tasks ADD COLUMN role TEXT NOT NULL DEFAULT 'work'");
7
+ if (!columns.some((c) => c.name === 'verification'))
8
+ db.exec('ALTER TABLE tasks ADD COLUMN verification TEXT');
9
+ if (columns.some((c) => c.name === 'title')) {
10
+ db.exec("UPDATE tasks SET role = 'finalization' WHERE title LIKE '[FINAL] %' AND role = 'work'");
11
+ }
12
+ }
13
+ db.exec(`
14
+ CREATE TABLE IF NOT EXISTS auto_loop_runs (
15
+ workspace_id TEXT PRIMARY KEY REFERENCES workspaces(id) ON DELETE CASCADE,
16
+ phase TEXT NOT NULL DEFAULT 'grooming',
17
+ state TEXT NOT NULL DEFAULT 'waiting',
18
+ reason TEXT,
19
+ iteration INTEGER NOT NULL DEFAULT 0,
20
+ diagnostic_attempts INTEGER NOT NULL DEFAULT 0,
21
+ current_task_id TEXT,
22
+ current_session_id TEXT,
23
+ updated_at TEXT NOT NULL
24
+ );
25
+ CREATE TABLE IF NOT EXISTS auto_loop_progress (
26
+ workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
27
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
28
+ milestone INTEGER NOT NULL DEFAULT 0,
29
+ PRIMARY KEY (workspace_id, task_id)
30
+ );
31
+ CREATE TABLE IF NOT EXISTS auto_loop_messages (
32
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
33
+ workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
34
+ client_message_id TEXT NOT NULL,
35
+ content TEXT NOT NULL,
36
+ source TEXT,
37
+ state TEXT NOT NULL DEFAULT 'pending',
38
+ session_id TEXT,
39
+ created_at TEXT NOT NULL,
40
+ UNIQUE (workspace_id, client_message_id)
41
+ );
42
+ CREATE INDEX IF NOT EXISTS idx_auto_loop_messages_pending ON auto_loop_messages(workspace_id, state, id);
43
+ `);
44
+ }
@@ -1,4 +1,6 @@
1
+ import { initAutoLoopSchema } from './auto-loop-schema.js';
1
2
  import { initMcpMessageSchema } from './mcp-message-schema.js';
3
+ import { initReviewReturnSchema } from './review-return-schema.js';
2
4
  import { initSchema } from './schema.js';
3
5
  import { initSearchSchema } from './search-schema.js';
4
6
  /** Ordered registry of all schema migrations. Append new entries at the end. */
@@ -744,6 +746,8 @@ export const migrations = [
744
746
  },
745
747
  { version: 42, name: 'index-logical-search-messages', migrate: initSearchSchema },
746
748
  { version: 43, name: 'durable-mcp-message-requests', migrate: initMcpMessageSchema },
749
+ { version: 44, name: 'review-session-returns', migrate: initReviewReturnSchema },
750
+ { version: 45, name: 'durable-auto-loop', migrate: initAutoLoopSchema },
747
751
  ];
748
752
  /** Current schema version — always equals the highest migration version. */
749
753
  export const SCHEMA_VERSION = migrations.length > 0 ? migrations[migrations.length - 1].version : 1;
@@ -0,0 +1,10 @@
1
+ export function initReviewReturnSchema(db) {
2
+ db.exec(`CREATE TABLE IF NOT EXISTS pending_review_returns (
3
+ workspace_id TEXT PRIMARY KEY REFERENCES workspaces(id) ON DELETE CASCADE,
4
+ review_session_id TEXT NOT NULL REFERENCES agent_sessions(id) ON DELETE CASCADE,
5
+ original_session_id TEXT NOT NULL REFERENCES agent_sessions(id) ON DELETE CASCADE,
6
+ original_configuration TEXT NOT NULL,
7
+ review_configuration TEXT NOT NULL,
8
+ created_at TEXT NOT NULL
9
+ )`);
10
+ }
@@ -1,4 +1,6 @@
1
+ import { initAutoLoopSchema } from './auto-loop-schema.js';
1
2
  import { initMcpMessageSchema } from './mcp-message-schema.js';
3
+ import { initReviewReturnSchema } from './review-return-schema.js';
2
4
  import { initSearchSchema } from './search-schema.js';
3
5
  /** Create all tables and indexes for a fresh install. Not used for upgrades -- see migrations.ts. */
4
6
  export function initSchema(db) {
@@ -225,4 +227,6 @@ export function initSchema(db) {
225
227
  `);
226
228
  initSearchSchema(db);
227
229
  initMcpMessageSchema(db);
230
+ initReviewReturnSchema(db);
231
+ initAutoLoopSchema(db);
228
232
  }
@@ -41,6 +41,7 @@ import { reconcileMessageRequests } from './services/mcp-message-request-service
41
41
  import { authorizeWsUpgrade, generateToken, getLanHostnames, getLanUrls, isAllowedOrigin, isAllowedRequestHost, resolveBindHost, resolveDevClientOrigin, resolveNetworkAccessEnvOverrides, resolveProxyHostname, } from './services/network-access-service.js';
42
42
  import { startPrWatcher, stopPrWatcher } from './services/pr-watcher-service.js';
43
43
  import * as quotaBackoffService from './services/quota-backoff-service.js';
44
+ import { reconcileReviewReturns } from './services/review-return-service.js';
44
45
  import { startSearchIndex, stopSearchIndex } from './services/search-service.js';
45
46
  import { getGlobalSettings, updateNetworkAccessSettings } from './services/settings-service.js';
46
47
  import { reloadDefaultTemplates } from './services/templates-service.js';
@@ -116,15 +117,7 @@ const dailyBackupScheduler = startDailyDbBackupScheduler(async () => {
116
117
  });
117
118
  // Initialize process cleanup, agent watchdog, PR watcher, and wakeup rehydration
118
119
  reconcileOrphanSessions();
119
- startWatchdog();
120
- wakeupService.rehydrate();
121
- autoLoopService.rehydrate();
122
- // Restore in-memory retry counts BEFORE re-arming the persisted backoff timers,
123
- // otherwise the next arm() after restart would compute the next ladder rung
124
- // from retryCount=0 and undo the progression.
125
- restoreRetryCountsFromDb();
126
- quotaBackoffService.restoreOnBoot((workspaceId, pending) => autoLoopService.onQuotaBackoffExpired(workspaceId, pending));
127
- cronService.restoreOnBoot();
120
+ reconcileReviewReturns();
128
121
  // Deliver any new default prompt templates to existing installs (seed-once via the
129
122
  // seededDefaultSlugs watermark; never overwrites or re-adds deleted defaults).
130
123
  try {
@@ -229,6 +222,14 @@ const server = serve({
229
222
  hostname: bindHost,
230
223
  }, (info) => {
231
224
  setBackendPort(info.port);
225
+ // Restored agents receive the actual listening endpoint, including an
226
+ // ephemeral or SERVER_PORT-selected port. Quota ownership comes first.
227
+ restoreRetryCountsFromDb();
228
+ quotaBackoffService.restoreOnBoot((workspaceId, pending) => autoLoopService.onQuotaBackoffExpired(workspaceId, pending));
229
+ autoLoopService.rehydrate();
230
+ wakeupService.rehydrate();
231
+ cronService.restoreOnBoot();
232
+ startWatchdog();
232
233
  startUpdateChecker();
233
234
  stopStartupSpinner();
234
235
  const settings = getGlobalSettings();
@@ -0,0 +1,51 @@
1
+ import { Hono } from 'hono';
2
+ import { listLoopMessages, resolveLoopMessage } from '../services/auto-loop-message-service.js';
3
+ import { deliverWorkspaceMessage } from '../services/workspace-message-service.js';
4
+ import { getWorkspace } from '../services/workspace-service.js';
5
+ import { assertWorkspaceLifecycleAvailable } from '../utils/workspace-lifecycle-guard.js';
6
+ const app = new Hono();
7
+ app.get('/:id/auto-loop/messages', (c) => {
8
+ try {
9
+ const id = c.req.param('id');
10
+ if (!getWorkspace(id))
11
+ return c.json({ error: 'Workspace not found' }, 404);
12
+ return c.json(listLoopMessages(id));
13
+ }
14
+ catch (error) {
15
+ return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
16
+ }
17
+ });
18
+ app.post('/:id/auto-loop/messages', async (c) => {
19
+ try {
20
+ const body = await c.req.json();
21
+ if (typeof body.content !== 'string' || typeof body.clientMessageId !== 'string')
22
+ return c.json({ error: 'content and clientMessageId are required' }, 400);
23
+ await deliverWorkspaceMessage(c.req.param('id'), {
24
+ content: body.content,
25
+ clientMessageId: body.clientMessageId,
26
+ delivery: 'next_iteration',
27
+ });
28
+ return c.json({ accepted: true });
29
+ }
30
+ catch (error) {
31
+ return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
32
+ }
33
+ });
34
+ app.patch('/:id/auto-loop/messages/:messageId', async (c) => {
35
+ try {
36
+ const id = c.req.param('id');
37
+ assertWorkspaceLifecycleAvailable(id);
38
+ const workspace = getWorkspace(id);
39
+ if (!workspace || workspace.archivedAt || workspace.worktreePurgedAt)
40
+ return c.json({ error: 'Workspace unavailable' }, 409);
41
+ const { action } = await c.req.json();
42
+ if (!['cancel', 'acknowledge', 'retry'].includes(action))
43
+ return c.json({ error: 'Invalid resolution' }, 400);
44
+ resolveLoopMessage(id, Number(c.req.param('messageId')), action);
45
+ return c.json({ ok: true });
46
+ }
47
+ catch (error) {
48
+ return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
49
+ }
50
+ });
51
+ export default app;