@loicngr/kobo 1.11.43 → 1.11.45

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 (166) hide show
  1. package/AGENTS.md +74 -1
  2. package/CHANGELOG.md +11 -0
  3. package/README.md +13 -1
  4. package/dist/mcp-server/kobo-tasks-handlers.js +33 -75
  5. package/dist/mcp-server/kobo-tasks-server.js +82 -9
  6. package/dist/server/db/auto-loop-schema.js +44 -0
  7. package/dist/server/db/migrations.js +18 -0
  8. package/dist/server/db/review-return-schema.js +10 -0
  9. package/dist/server/db/schema.js +8 -1
  10. package/dist/server/db/session-handoff-schema.js +24 -0
  11. package/dist/server/index.js +12 -9
  12. package/dist/server/routes/auto-loop-messages.js +51 -0
  13. package/dist/server/routes/documents.js +1 -1
  14. package/dist/server/routes/workspaces.js +119 -145
  15. package/dist/server/services/agent/engines/claude-code/engine.js +16 -0
  16. package/dist/server/services/agent/engines/codex/engine.js +92 -31
  17. package/dist/server/services/agent/orchestrator.js +298 -96
  18. package/dist/server/services/agent/session-controller.js +42 -2
  19. package/dist/server/services/auto-loop-message-service.js +79 -0
  20. package/dist/server/services/auto-loop-service.js +225 -86
  21. package/dist/server/services/auto-loop-state-service.js +30 -0
  22. package/dist/server/services/cron-service.js +30 -16
  23. package/dist/server/services/engine-handoff-service.js +5 -5
  24. package/dist/server/services/quota-backoff-service.js +42 -10
  25. package/dist/server/services/review-return-service.js +64 -0
  26. package/dist/server/services/review-service.js +187 -0
  27. package/dist/server/services/session-activity-service.js +17 -0
  28. package/dist/server/services/session-handoff-runtime.js +26 -0
  29. package/dist/server/services/session-handoff-service.js +479 -0
  30. package/dist/server/services/settings-service.js +3 -3
  31. package/dist/server/services/task-mutations.js +186 -0
  32. package/dist/server/services/wakeup-service.js +24 -1
  33. package/dist/server/services/workspace-message-service.js +16 -2
  34. package/dist/server/services/workspace-service.js +28 -36
  35. package/dist/server/utils/workspace-lifecycle-guard.js +35 -20
  36. package/dist/shared/auto-loop-prompts.js +22 -11
  37. package/dist/shared/auto-loop-types.js +1 -0
  38. package/dist/shared/document-roots.js +2 -0
  39. package/dist/shared/review.js +6 -0
  40. package/dist/shared/session-handoff.js +4 -0
  41. package/dist/shared/task-verification.js +11 -0
  42. package/package.json +12 -12
  43. package/src/client/dist/pwa/assets/ActivityFeed-CLXjdqS8.js +8 -0
  44. package/src/client/dist/pwa/assets/{ChangelogPage-DX1WGFea.js → ChangelogPage-CnXTnFhe.js} +1 -1
  45. package/src/client/dist/pwa/assets/{ClosePopup-BqW5dy-O.js → ClosePopup-CWjIRHxu.js} +1 -1
  46. package/src/client/dist/pwa/assets/{ComparisonPanel-B4ne55Xj.js → ComparisonPanel-CKylN45q.js} +2 -2
  47. package/src/client/dist/pwa/assets/CreatePage-SoCcvxuD.js +2 -0
  48. package/src/client/dist/pwa/assets/{DashboardPage-Cv6HJEcO.js → DashboardPage-CZQLfoV1.js} +1 -1
  49. package/src/client/dist/pwa/assets/{DiffViewer-COwGxPt6.js → DiffViewer-Csf3WmLc.js} +4 -4
  50. package/src/client/dist/pwa/assets/{DocumentsPanel-DCXpLLb7.js → DocumentsPanel-BeCttB-x.js} +1 -1
  51. package/src/client/dist/pwa/assets/{DrawerToggleButton-DRL3RCkM.js → DrawerToggleButton-BJBBYeOc.js} +1 -1
  52. package/src/client/dist/pwa/assets/GitPanel-B-ZHgghQ.js +3 -0
  53. package/src/client/dist/pwa/assets/{HealthPage-XrOuMoDJ.js → HealthPage-l88TEk-k.js} +1 -1
  54. package/src/client/dist/pwa/assets/MainLayout-BAAQLq95.css +1 -0
  55. package/src/client/dist/pwa/assets/MainLayout-Cf4Q9x1u.js +2 -0
  56. package/src/client/dist/pwa/assets/{QBadge-DbkxGtht.js → QBadge-FWC1zDCq.js} +1 -1
  57. package/src/client/dist/pwa/assets/{QBanner-DhqH95GW.js → QBanner-CcRI_QIy.js} +1 -1
  58. package/src/client/dist/pwa/assets/QBtn-DEdx_Y1V.js +1 -0
  59. package/src/client/dist/pwa/assets/{QBtnDropdown-163oM6T_.js → QBtnDropdown-44Mj-y_1.js} +1 -1
  60. package/src/client/dist/pwa/assets/{QBtnGroup-DELKYo3u.js → QBtnGroup-CAoD6G0Q.js} +1 -1
  61. package/src/client/dist/pwa/assets/{QBtnToggle-BI3k3Ch2.js → QBtnToggle-C_hQ2UCQ.js} +1 -1
  62. package/src/client/dist/pwa/assets/{QChip-owciRuZu.js → QChip-DCPUhqXX.js} +1 -1
  63. package/src/client/dist/pwa/assets/QDrawer-B4qHw8wt.js +1 -0
  64. package/src/client/dist/pwa/assets/QExpansionItem-Bok1m8Wi.js +1 -0
  65. package/src/client/dist/pwa/assets/{QItemSection-D_5_0UrX.js → QItemSection-BhqphaH6.js} +1 -1
  66. package/src/client/dist/pwa/assets/{QList-qupzuEqO.js → QList-CHm4JNP0.js} +1 -1
  67. package/src/client/dist/pwa/assets/{QMarkupTable-B9QuKtK_.js → QMarkupTable-CNpiA9oq.js} +1 -1
  68. package/src/client/dist/pwa/assets/{QPage-DaX9sMQC.js → QPage-BXBw2Alx.js} +1 -1
  69. package/src/client/dist/pwa/assets/QSelect-BbtD5Iks.js +1 -0
  70. package/src/client/dist/pwa/assets/{QSpinnerDots-DnCV3VzI.js → QSpinnerDots-BkFiCsH3.js} +1 -1
  71. package/src/client/dist/pwa/assets/{QTooltip-BQWY7aeM.js → QTooltip-BDihn2aY.js} +1 -1
  72. package/src/client/dist/pwa/assets/SchedulePanel-DiWCDNZ8.js +1 -0
  73. package/src/client/dist/pwa/assets/{SearchPage-DOHkP5lj.js → SearchPage-BwZ2TX1G.js} +1 -1
  74. package/src/client/dist/pwa/assets/SettingsPage-BUh0WfNf.js +9 -0
  75. package/src/client/dist/pwa/assets/{SplitWorkspacePage-DL4kp9Ah.js → SplitWorkspacePage-Bg6nKBcA.js} +1 -1
  76. package/src/client/dist/pwa/assets/{TerminalPanel-DHDnyiYh.js → TerminalPanel-Cq-FR7A7.js} +9 -9
  77. package/src/client/dist/pwa/assets/{TouchPan-BnHXDR6F.js → TouchPan-BQ1cvzRC.js} +1 -1
  78. package/src/client/dist/pwa/assets/{TourReplayButton-ClVcB6mO.js → TourReplayButton-Dz1yEz_z.js} +1 -1
  79. package/src/client/dist/pwa/assets/{WorkspaceAttentionLabels-Bxe6oIJI.js → WorkspaceAttentionLabels-BCVOP8vp.js} +1 -1
  80. package/src/client/dist/pwa/assets/WorkspacePage-BbUvR1Vf.css +1 -0
  81. package/src/client/dist/pwa/assets/WorkspacePage-Cl8gap4b.js +4 -0
  82. package/src/client/dist/pwa/assets/{browser-Bkx4fbxl.js → browser-BmWgBx9M.js} +1 -1
  83. package/src/client/dist/pwa/assets/build-path-tree-6EjBmYYY.js +1 -0
  84. package/src/client/dist/pwa/assets/de-COObokpu.js +17 -0
  85. package/src/client/dist/pwa/assets/documents-BZ8bwcjr.js +1 -0
  86. package/src/client/dist/pwa/assets/{editor.api-DreEG0If.js → editor.api-CK7--ckY.js} +1 -1
  87. package/src/client/dist/pwa/assets/editor.worker-iO5iN0DB.js +33 -0
  88. package/src/client/dist/pwa/assets/efforts-Bw0o2Bud.js +9 -0
  89. package/src/client/dist/pwa/assets/engineFeatures-B-9jHocY.css +1 -0
  90. package/src/client/dist/pwa/assets/engineFeatures-DQO9HHXI.js +1 -0
  91. package/src/client/dist/pwa/assets/es-By2Q9Tca.js +17 -0
  92. package/src/client/dist/pwa/assets/expand-template-C8VyjASo.js +1 -0
  93. package/src/client/dist/pwa/assets/{format-B7z3F-Mw.js → format-LtTLLitJ.js} +3 -3
  94. package/src/client/dist/pwa/assets/{formatters-BTBiU0v3.js → formatters-Bl9zA7Ai.js} +1 -1
  95. package/src/client/dist/pwa/assets/fr-D-sd0ddn.js +17 -0
  96. package/src/client/dist/pwa/assets/{freemarker2-BZNTVQ3K.js → freemarker2-DxezMaje.js} +1 -1
  97. package/src/client/dist/pwa/assets/{handlebars-k8tN0dI3.js → handlebars-DK_Bj6Rx.js} +1 -1
  98. package/src/client/dist/pwa/assets/{html-iZL3bbXq.js → html-DFfsfStD.js} +1 -1
  99. package/src/client/dist/pwa/assets/i18n-aDWy4KHt.js +1 -0
  100. package/src/client/dist/pwa/assets/index-0wtBh1Xt.css +1 -0
  101. package/src/client/dist/pwa/assets/index-BLd8DAzg.js +20 -0
  102. package/src/client/dist/pwa/assets/it-CeK5lyIA.js +17 -0
  103. package/src/client/dist/pwa/assets/{javascript-TV6gg0MO.js → javascript-DuNbcaL7.js} +1 -1
  104. package/src/client/dist/pwa/assets/{liquid-Cifa5ujF.js → liquid-CM3JKyhv.js} +1 -1
  105. package/src/client/dist/pwa/assets/{mdx-Bnj_8yGE.js → mdx-BG3zYwNF.js} +1 -1
  106. package/src/client/dist/pwa/assets/{monaco-contributions-pEh0n9Wk.js → monaco-contributions-DLscZGH-.js} +1 -1
  107. package/src/client/dist/pwa/assets/monaco.contribution-DnhZHwXT.js +2 -0
  108. package/src/client/dist/pwa/assets/{network-auth-3_5p4fdc.js → network-auth-BJiPbO5l.js} +1 -1
  109. package/src/client/dist/pwa/assets/{notify-theme-D_nf9AeF.js → notify-theme-D4p3qPkp.js} +1 -1
  110. package/src/client/dist/pwa/assets/{pending-diff-open-CUnxC1xw.js → pending-diff-open-dYIFdS4r.js} +1 -1
  111. package/src/client/dist/pwa/assets/permissionModes-B7fuUegU.js +36 -0
  112. package/src/client/dist/pwa/assets/{pinia-BKqmoVru.js → pinia-CTJ3OfP3.js} +1 -1
  113. package/src/client/dist/pwa/assets/{private.use-form-C7umbGO-.js → private.use-form-Bk8ti264.js} +1 -1
  114. package/src/client/dist/pwa/assets/{project-color-CihwpDAA.js → project-color-R_1je4vW.js} +1 -1
  115. package/src/client/dist/pwa/assets/{python-DOoNBSNL.js → python-COs6pHk2.js} +1 -1
  116. package/src/client/dist/pwa/assets/{razor-Bvq5vVpJ.js → razor-Jl1VqMsk.js} +1 -1
  117. package/src/client/dist/pwa/assets/reactivity.esm-bundler-Bo9anJjg.js +1 -0
  118. package/src/client/dist/pwa/assets/render-chat-markdown-AaOo0Tgq.js +67 -0
  119. package/src/client/dist/pwa/assets/{runtime-core.esm-bundler-CDnVUfeZ.js → runtime-core.esm-bundler-CNFKm4ij.js} +1 -1
  120. package/src/client/dist/pwa/assets/{typescript-VVzxoeyM.js → typescript-BU_TUuAM.js} +1 -1
  121. package/src/client/dist/pwa/assets/{use-checkbox-FnauAgTp.js → use-checkbox-CnvHBhSK.js} +1 -1
  122. package/src/client/dist/pwa/assets/{use-id-BIuaWKkB.js → use-id-wXd4c8FA.js} +1 -1
  123. package/src/client/dist/pwa/assets/use-is-mobile-DpKI3XMy.js +1 -0
  124. package/src/client/dist/pwa/assets/{use-search-index-status-DtLzygcy.js → use-search-index-status-Bluf9L99.js} +1 -1
  125. package/src/client/dist/pwa/assets/use-slide-transition-CiaDm-tR.js +1 -0
  126. package/src/client/dist/pwa/assets/use-tours-CVn874kD.js +2 -0
  127. package/src/client/dist/pwa/assets/{workspace-templates-1ui80Kqo.js → workspace-templates-BCKR8TXr.js} +1 -1
  128. package/src/client/dist/pwa/assets/{xml-CzGFIxQa.js → xml-gFFsobxE.js} +1 -1
  129. package/src/client/dist/pwa/assets/{yaml-Bl7nDJeC.js → yaml-DaXaqaGD.js} +1 -1
  130. package/src/client/dist/pwa/index.html +10 -10
  131. package/src/client/dist/pwa/sw.js +1 -1
  132. package/src/mcp-server/README.md +8 -1
  133. package/src/mcp-server/kobo-tasks-handlers.ts +72 -105
  134. package/src/mcp-server/kobo-tasks-server.ts +94 -9
  135. package/src/client/dist/pwa/assets/ActivityFeed-D1e-zV3O.js +0 -8
  136. package/src/client/dist/pwa/assets/CreatePage-Bx_GpUni.js +0 -2
  137. package/src/client/dist/pwa/assets/GitPanel-C9mz4GXF.js +0 -3
  138. package/src/client/dist/pwa/assets/MainLayout-BwV89R-l.css +0 -1
  139. package/src/client/dist/pwa/assets/MainLayout-CwWavQjV.js +0 -2
  140. package/src/client/dist/pwa/assets/QBtn-CZaBclNw.js +0 -1
  141. package/src/client/dist/pwa/assets/QDrawer-DNEj8wLZ.js +0 -1
  142. package/src/client/dist/pwa/assets/QSelect-BUlOQe5k.js +0 -1
  143. package/src/client/dist/pwa/assets/SchedulePanel-Bos94OyR.js +0 -1
  144. package/src/client/dist/pwa/assets/SettingsPage-DtGsd0XH.js +0 -9
  145. package/src/client/dist/pwa/assets/WorkspacePage-Clc4Fl3B.js +0 -4
  146. package/src/client/dist/pwa/assets/WorkspacePage-DCg9zSxz.css +0 -1
  147. package/src/client/dist/pwa/assets/build-path-tree-BpSY7N5O.js +0 -1
  148. package/src/client/dist/pwa/assets/de-BU0dILBs.js +0 -17
  149. package/src/client/dist/pwa/assets/documents-BIlWh3o-.js +0 -1
  150. package/src/client/dist/pwa/assets/editor.worker-CwQAd_xr.js +0 -30
  151. package/src/client/dist/pwa/assets/efforts-D_yTzymv.js +0 -9
  152. package/src/client/dist/pwa/assets/engineFeatures-C33LTVkK.js +0 -1
  153. package/src/client/dist/pwa/assets/es-CNp0s9na.js +0 -17
  154. package/src/client/dist/pwa/assets/expand-template-wRhAXT-1.js +0 -1
  155. package/src/client/dist/pwa/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNa-Dr0goTwe.woff +0 -0
  156. package/src/client/dist/pwa/assets/fr-RqJyWSdm.js +0 -17
  157. package/src/client/dist/pwa/assets/i18n--PCet00a.js +0 -1
  158. package/src/client/dist/pwa/assets/index-CPfrY38K.js +0 -20
  159. package/src/client/dist/pwa/assets/index-CUoUy6tH.css +0 -1
  160. package/src/client/dist/pwa/assets/it-DSxNuCwL.js +0 -17
  161. package/src/client/dist/pwa/assets/monaco.contribution-D7NmJUJH.js +0 -2
  162. package/src/client/dist/pwa/assets/permissionModes-DC51Pz84.js +0 -36
  163. package/src/client/dist/pwa/assets/reactivity.esm-bundler-DEJj9cFv.js +0 -1
  164. package/src/client/dist/pwa/assets/render-chat-markdown-BQ_saiy5.js +0 -67
  165. package/src/client/dist/pwa/assets/use-is-mobile-CWjqe8gC.js +0 -1
  166. package/src/client/dist/pwa/assets/use-tours-BKeYrJaQ.js +0 -2
@@ -0,0 +1,186 @@
1
+ import { nanoid } from 'nanoid';
2
+ export class TaskValidationError extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = 'TaskValidationError';
6
+ }
7
+ }
8
+ function validateVerification(input, requirePassed) {
9
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
10
+ throw new TaskValidationError('Task verification is required before marking an auto-loop task done');
11
+ }
12
+ const proof = input;
13
+ if (typeof proof.method !== 'string' ||
14
+ !proof.method.trim() ||
15
+ typeof proof.summary !== 'string' ||
16
+ !proof.summary.trim() ||
17
+ !Array.isArray(proof.checks) ||
18
+ proof.checks.length === 0 ||
19
+ proof.checks.some((check) => !check ||
20
+ typeof check.name !== 'string' ||
21
+ !check.name.trim() ||
22
+ !['passed', 'failed', 'not_run'].includes(check.status) ||
23
+ (requirePassed && check.status !== 'passed'))) {
24
+ throw new TaskValidationError('Task verification requires a method, summary and named checks; every check must have passed before completion');
25
+ }
26
+ return {
27
+ method: proof.method.trim(),
28
+ summary: proof.summary.trim(),
29
+ checks: proof.checks.map((check) => ({ name: check.name.trim(), status: check.status })),
30
+ };
31
+ }
32
+ function validateOrder(input) {
33
+ if (input.sortOrder !== undefined && (!Number.isSafeInteger(input.sortOrder) || input.sortOrder < 0)) {
34
+ throw new TaskValidationError('sort_order must be a non-negative safe integer');
35
+ }
36
+ if (input.afterTaskId !== undefined && (typeof input.afterTaskId !== 'string' || !input.afterTaskId)) {
37
+ throw new TaskValidationError('after_task_id must be a non-empty task id');
38
+ }
39
+ if (input.sortOrder !== undefined && input.afterTaskId !== undefined) {
40
+ throw new TaskValidationError('Provide either sort_order or after_task_id, not both');
41
+ }
42
+ if ('isAcceptanceCriterion' in input &&
43
+ input.isAcceptanceCriterion !== undefined &&
44
+ typeof input.isAcceptanceCriterion !== 'boolean') {
45
+ throw new TaskValidationError('is_acceptance_criterion must be a boolean');
46
+ }
47
+ }
48
+ /** Task-list changes make the previous final review obsolete, even while the loop is paused. */
49
+ export function invalidateTaskFinalization(db, workspaceId, verifiedTaskId = '') {
50
+ db.prepare(`UPDATE tasks SET status = 'pending', verification = NULL, updated_at = ?
51
+ WHERE workspace_id = ? AND role = 'finalization' AND status = 'done' AND id <> ?`).run(new Date().toISOString(), workspaceId, verifiedTaskId);
52
+ }
53
+ function readTask(db, workspaceId, taskId) {
54
+ const task = db.prepare('SELECT * FROM tasks WHERE id = ? AND workspace_id = ?').get(taskId, workspaceId);
55
+ if (!task)
56
+ throw new Error(`Task '${taskId}' not found in workspace '${workspaceId}'`);
57
+ return task;
58
+ }
59
+ /** Insert/move after an existing sibling and normalize positions within the caller's transaction. */
60
+ function moveAfter(db, workspaceId, taskId, afterTaskId) {
61
+ readTask(db, workspaceId, afterTaskId);
62
+ if (taskId === afterTaskId)
63
+ throw new TaskValidationError('A task cannot be placed after itself');
64
+ const tasks = db
65
+ .prepare('SELECT id, sort_order FROM tasks WHERE workspace_id = ? ORDER BY sort_order, rowid')
66
+ .all(workspaceId);
67
+ const ordered = tasks.filter((task) => task.id !== taskId);
68
+ ordered.splice(ordered.findIndex((task) => task.id === afterTaskId) + 1, 0, tasks.find((task) => task.id === taskId));
69
+ const changed = tasks.some((task, index) => task.id !== ordered[index]?.id);
70
+ if (!changed)
71
+ return false;
72
+ const update = db.prepare('UPDATE tasks SET sort_order = ?, updated_at = ? WHERE id = ?');
73
+ const now = new Date().toISOString();
74
+ ordered.forEach((task, index) => {
75
+ update.run(index, now, task.id);
76
+ });
77
+ return true;
78
+ }
79
+ export function createTaskRecord(db, workspaceId, input) {
80
+ if (typeof input.title !== 'string' || !input.title.trim())
81
+ throw new TaskValidationError('title is required');
82
+ validateOrder(input);
83
+ if (input.role !== undefined && input.role !== 'work' && input.role !== 'finalization') {
84
+ throw new TaskValidationError('Task role must be work or finalization');
85
+ }
86
+ return db
87
+ .transaction(() => {
88
+ if (!db.prepare('SELECT 1 FROM workspaces WHERE id = ?').get(workspaceId)) {
89
+ throw new Error(`Workspace '${workspaceId}' not found`);
90
+ }
91
+ if (input.afterTaskId !== undefined)
92
+ readTask(db, workspaceId, input.afterTaskId);
93
+ const title = input.title.trim();
94
+ const role = input.role ?? (title.startsWith('[FINAL] ') ? 'finalization' : 'work');
95
+ const max = db
96
+ .prepare('SELECT COALESCE(MAX(sort_order), -1) AS value FROM tasks WHERE workspace_id = ?')
97
+ .get(workspaceId).value;
98
+ const order = input.sortOrder ?? max + 1;
99
+ const now = new Date().toISOString();
100
+ if (input.sortOrder !== undefined) {
101
+ db.prepare('UPDATE tasks SET sort_order = sort_order + 1 WHERE workspace_id = ? AND sort_order >= ?').run(workspaceId, order);
102
+ }
103
+ const id = nanoid();
104
+ db.prepare(`INSERT INTO tasks (id, workspace_id, title, status, is_acceptance_criterion, sort_order, role, verification, created_at, updated_at)
105
+ VALUES (?, ?, ?, 'pending', ?, ?, ?, NULL, ?, ?)`).run(id, workspaceId, title, input.isAcceptanceCriterion ? 1 : 0, order, role, now, now);
106
+ if (input.afterTaskId !== undefined)
107
+ moveAfter(db, workspaceId, id, input.afterTaskId);
108
+ if (role === 'work')
109
+ invalidateTaskFinalization(db, workspaceId);
110
+ return readTask(db, workspaceId, id);
111
+ })
112
+ .immediate();
113
+ }
114
+ export function updateTaskRecord(db, workspaceId, taskId, input) {
115
+ validateOrder(input);
116
+ if (input.title !== undefined && (typeof input.title !== 'string' || !input.title.trim())) {
117
+ throw new TaskValidationError('Task title cannot be empty');
118
+ }
119
+ if (input.status !== undefined && !['pending', 'in_progress', 'done'].includes(input.status)) {
120
+ throw new TaskValidationError(`Invalid status '${input.status}'. Must be one of: pending, in_progress, done`);
121
+ }
122
+ if (!['title', 'status', 'isAcceptanceCriterion', 'sortOrder', 'afterTaskId', 'verification'].some((key) => input[key] !== undefined)) {
123
+ throw new TaskValidationError('No fields to update (provide title, status, is_acceptance_criterion, order, or verification)');
124
+ }
125
+ return db
126
+ .transaction(() => {
127
+ const task = readTask(db, workspaceId, taskId);
128
+ const autoLoop = db.prepare('SELECT auto_loop FROM workspaces WHERE id = ?').get(workspaceId)
129
+ .auto_loop === 1;
130
+ const title = input.title?.trim() ?? task.title;
131
+ const isAC = input.isAcceptanceCriterion === undefined ? task.is_acceptance_criterion : Number(input.isAcceptanceCriterion);
132
+ const scopeChanged = title !== task.title || isAC !== task.is_acceptance_criterion;
133
+ const status = input.status ??
134
+ (scopeChanged && task.status === 'done' && (autoLoop || task.role === 'finalization') ? 'pending' : task.status);
135
+ const completing = status === 'done' && (input.status === 'done' || input.verification !== undefined || scopeChanged);
136
+ let verification = scopeChanged || status !== 'done' ? null : task.verification;
137
+ if (input.verification !== undefined)
138
+ verification = JSON.stringify(validateVerification(input.verification, completing));
139
+ if (completing && autoLoop && input.verification === undefined)
140
+ validateVerification(undefined, true);
141
+ if (completing &&
142
+ task.role === 'finalization' &&
143
+ db
144
+ .prepare("SELECT 1 FROM tasks WHERE workspace_id = ? AND role = 'work' AND status <> 'done' LIMIT 1")
145
+ .get(workspaceId)) {
146
+ throw new TaskValidationError('Finalization cannot be completed while work tasks or acceptance criteria are pending');
147
+ }
148
+ if (completing &&
149
+ task.role === 'finalization' &&
150
+ db
151
+ .prepare("SELECT 1 FROM auto_loop_messages WHERE workspace_id = ? AND state IN ('pending', 'unknown') LIMIT 1")
152
+ .get(workspaceId)) {
153
+ throw new TaskValidationError('Finalization cannot be completed while user instructions are pending or their delivery is unknown');
154
+ }
155
+ let orderChanged = false;
156
+ if (input.afterTaskId !== undefined)
157
+ orderChanged = moveAfter(db, workspaceId, taskId, input.afterTaskId);
158
+ if (input.sortOrder !== undefined && input.sortOrder !== task.sort_order) {
159
+ // Moving to an occupied position shifts the intervening siblings only.
160
+ if (input.sortOrder < task.sort_order) {
161
+ db.prepare('UPDATE tasks SET sort_order = sort_order + 1 WHERE workspace_id = ? AND id <> ? AND sort_order >= ? AND sort_order < ?').run(workspaceId, taskId, input.sortOrder, task.sort_order);
162
+ }
163
+ else {
164
+ db.prepare('UPDATE tasks SET sort_order = sort_order - 1 WHERE workspace_id = ? AND id <> ? AND sort_order > ? AND sort_order <= ?').run(workspaceId, taskId, task.sort_order, input.sortOrder);
165
+ }
166
+ db.prepare('UPDATE tasks SET sort_order = ? WHERE id = ?').run(input.sortOrder, taskId);
167
+ orderChanged = true;
168
+ }
169
+ db.prepare('UPDATE tasks SET title = ?, status = ?, is_acceptance_criterion = ?, verification = ?, updated_at = ? WHERE id = ?').run(title, status, isAC, verification, new Date().toISOString(), taskId);
170
+ if (scopeChanged || orderChanged || (task.status === 'done' && status !== 'done')) {
171
+ // Fresh evidence supplied with this exact edit validates its new scope;
172
+ // all other completed finalizations still describe the previous state.
173
+ invalidateTaskFinalization(db, workspaceId, completing && input.verification !== undefined ? taskId : '');
174
+ }
175
+ return readTask(db, workspaceId, taskId);
176
+ })
177
+ .immediate();
178
+ }
179
+ export function deleteTaskRecord(db, workspaceId, taskId) {
180
+ db.transaction(() => {
181
+ const task = readTask(db, workspaceId, taskId);
182
+ db.prepare('DELETE FROM tasks WHERE id = ?').run(taskId);
183
+ if (task.role === 'work')
184
+ invalidateTaskFinalization(db, workspaceId);
185
+ }).immediate();
186
+ }
@@ -3,6 +3,7 @@ import { slugifyProjectName } from '../utils/project-slug.js';
3
3
  import { isWorkspaceLifecycleBusy } from '../utils/workspace-lifecycle-guard.js';
4
4
  import { resolveWorkspaceWorktreePath } from '../utils/worktree-paths.js';
5
5
  import * as orchestrator from './agent/orchestrator.js';
6
+ import * as autoLoopService from './auto-loop-service.js';
6
7
  import * as settingsService from './settings-service.js';
7
8
  import { emitEphemeral } from './websocket-service.js';
8
9
  const MIN_DELAY_SECONDS = 60;
@@ -75,6 +76,14 @@ export function schedule(workspaceId, delaySeconds, prompt, reason, agentSession
75
76
  console.error('[wakeup-service] schedule failed:', err);
76
77
  }
77
78
  }
79
+ /** Move a source conversation's pending check only once its handoff succeeds. */
80
+ export function transferSession(workspaceId, sourceSessionId, targetSessionId) {
81
+ if (!sourceSessionId)
82
+ return;
83
+ getDb()
84
+ .prepare('UPDATE pending_wakeups SET agent_session_id = ? WHERE workspace_id = ? AND agent_session_id = ?')
85
+ .run(targetSessionId, workspaceId, sourceSessionId);
86
+ }
78
87
  /** Cancel any pending wakeup for the workspace. Idempotent. */
79
88
  export function cancel(workspaceId, reason) {
80
89
  try {
@@ -184,13 +193,27 @@ function fire(workspaceId) {
184
193
  return;
185
194
  }
186
195
  const wsRow = db
187
- .prepare(`SELECT project_path, working_branch, worktree_path, model, agent_permission_mode, reasoning_effort
196
+ .prepare(`SELECT project_path, working_branch, worktree_path, model, agent_permission_mode, reasoning_effort, auto_loop
188
197
  FROM workspaces WHERE id = ?`)
189
198
  .get(workspaceId);
190
199
  if (!wsRow) {
191
200
  emitEphemeral(workspaceId, 'wakeup:skipped', { reason: 'fire-failed' });
192
201
  return;
193
202
  }
203
+ if (wsRow.auto_loop !== 1 && !autoLoopService.canStartAutomatically(workspaceId)) {
204
+ defer(workspaceId, row);
205
+ return;
206
+ }
207
+ if (wsRow.auto_loop === 1) {
208
+ if (!autoLoopService.queueInstruction(workspaceId, row.prompt, `wakeup:${row.created_at}`)) {
209
+ defer(workspaceId, row);
210
+ return;
211
+ }
212
+ failedRetries.delete(workspaceId);
213
+ db.prepare('DELETE FROM pending_wakeups WHERE workspace_id = ?').run(workspaceId);
214
+ emitEphemeral(workspaceId, 'wakeup:fired', {});
215
+ return;
216
+ }
194
217
  const globalSettings = settingsService.getGlobalSettings();
195
218
  const projectSettings = settingsService.getProjectSettings(wsRow.project_path);
196
219
  const projectSlug = globalSettings.worktreesPrefixByProject
@@ -1,7 +1,9 @@
1
1
  import { assertWorkspaceLifecycleAvailable } from '../utils/workspace-lifecycle-guard.js';
2
2
  import { isAgentUnavailableError, isShuttingDown, sendMessage, startAgent } from './agent/orchestrator.js';
3
+ import { hasLoopMessage } from './auto-loop-message-service.js';
3
4
  import * as autoLoop from './auto-loop-service.js';
4
5
  import { createChatReceipt } from './chat-receipt-service.js';
6
+ import { emitEphemeral } from './websocket-service.js';
5
7
  import { getActiveSession, getWorkspace, updateWorkspaceStatus } from './workspace-service.js';
6
8
  /** Shared delivery for WebSocket chat and external MCP clients. */
7
9
  export async function deliverWorkspaceMessage(workspaceId, message, hooks) {
@@ -18,6 +20,18 @@ export async function deliverWorkspaceMessage(workspaceId, message, hooks) {
18
20
  throw new Error(`Workspace '${workspaceId}' not found`);
19
21
  if (workspace.archivedAt || workspace.worktreePurgedAt)
20
22
  throw new Error('Restore the workspace before sending a message');
23
+ if (message.delivery === 'next_iteration') {
24
+ if (!message.clientMessageId)
25
+ throw new Error('clientMessageId is required for queued delivery');
26
+ const acceptedBefore = hasLoopMessage(workspaceId, message.clientMessageId);
27
+ if (!autoLoop.queueInstruction(workspaceId, message.content, message.clientMessageId, message.source))
28
+ throw new Error('Auto-loop is not enabled');
29
+ if (!acceptedBefore)
30
+ receipt.accept();
31
+ else
32
+ emitEphemeral(workspaceId, 'chat:accepted', { clientMessageId: message.clientMessageId });
33
+ return {};
34
+ }
21
35
  if (workspace.status === 'compacting') {
22
36
  reason = 'compacting';
23
37
  throw new Error('Workspace is compacting its context; wait until compaction finishes before sending a message');
@@ -27,8 +41,6 @@ export async function deliverWorkspaceMessage(workspaceId, message, hooks) {
27
41
  throw new Error('Answer the pending question or permission request before sending a chat message');
28
42
  }
29
43
  const loop = autoLoop.getStatus(workspaceId);
30
- if (loop.auto_loop && loop.auto_loop_ready)
31
- autoLoop.disable(workspaceId, 'user-action');
32
44
  let sessionId = message.sessionId ?? getActiveSession(workspaceId)?.id;
33
45
  try {
34
46
  if (hooks?.beforeDispatch)
@@ -48,6 +60,8 @@ export async function deliverWorkspaceMessage(workspaceId, message, hooks) {
48
60
  updateWorkspaceStatus(workspaceId, 'executing');
49
61
  sessionId = message.sessionId ?? started.agentSessionId;
50
62
  }
63
+ if (loop.auto_loop && loop.auto_loop_ready)
64
+ autoLoop.disable(workspaceId, 'user-action');
51
65
  receipt.accept(sessionId);
52
66
  return { sessionId };
53
67
  }
@@ -1,4 +1,5 @@
1
1
  import { nanoid } from 'nanoid';
2
+ import { parseTaskVerification } from '../../shared/task-verification.js';
2
3
  import { getDb } from '../db/index.js';
3
4
  import { isValidBranchName } from '../utils/git-ops.js';
4
5
  import { resolveWorkspaceWorktreePath } from '../utils/worktree-paths.js';
@@ -6,6 +7,8 @@ import * as orchestrator from './agent/orchestrator.js';
6
7
  import * as autoLoopService from './auto-loop-service.js';
7
8
  import * as cronService from './cron-service.js';
8
9
  import * as quotaBackoffService from './quota-backoff-service.js';
10
+ import { SESSION_RECENCY_ORDER } from './session-activity-service.js';
11
+ import { createTaskRecord, deleteTaskRecord, updateTaskRecord } from './task-mutations.js';
9
12
  import * as wakeupService from './wakeup-service.js';
10
13
  import { emitEphemeral } from './websocket-service.js';
11
14
  /** Allowed status transitions per current status. Enforced by updateWorkspaceStatus. */
@@ -112,6 +115,8 @@ function mapTask(row) {
112
115
  status: row.status,
113
116
  isAcceptanceCriterion: row.is_acceptance_criterion === 1,
114
117
  sortOrder: row.sort_order,
118
+ role: row.role,
119
+ verification: parseTaskVerification(row.verification),
115
120
  createdAt: row.created_at,
116
121
  updatedAt: row.updated_at,
117
122
  };
@@ -590,18 +595,10 @@ export function deleteWorkspace(id) {
590
595
  /** Create a new task under a workspace. Throws if the workspace does not exist. */
591
596
  export function createTask(workspaceId, data) {
592
597
  const db = getDb();
593
- const exists = db.prepare('SELECT 1 FROM workspaces WHERE id = ?').get(workspaceId);
594
- if (!exists) {
598
+ if (!db.prepare('SELECT 1 FROM workspaces WHERE id = ?').get(workspaceId)) {
595
599
  throw new Error(`Workspace not found: '${workspaceId}'`);
596
600
  }
597
- const now = new Date().toISOString();
598
- const id = nanoid();
599
- db.prepare(`
600
- INSERT INTO tasks (id, workspace_id, title, status, is_acceptance_criterion, sort_order, created_at, updated_at)
601
- VALUES (?, ?, ?, 'pending', ?, ?, ?, ?)
602
- `).run(id, workspaceId, data.title, data.isAcceptanceCriterion ? 1 : 0, data.sortOrder ?? 0, now, now);
603
- const row = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id);
604
- return mapTask(row);
601
+ return mapTask(createTaskRecord(db, workspaceId, data));
605
602
  }
606
603
  /** Fetch a single task by ID scoped to a workspace, or null if not found. */
607
604
  export function getTask(taskId, workspaceId) {
@@ -613,39 +610,32 @@ export function getTask(taskId, workspaceId) {
613
610
  export function listTasks(workspaceId) {
614
611
  const db = getDb();
615
612
  const rows = db
616
- .prepare('SELECT * FROM tasks WHERE workspace_id = ? ORDER BY sort_order ASC')
613
+ .prepare('SELECT * FROM tasks WHERE workspace_id = ? ORDER BY sort_order ASC, rowid ASC')
617
614
  .all(workspaceId);
618
615
  return rows.map(mapTask);
619
616
  }
620
- /** Update a task's status (pending, in_progress, done). */
621
- export function updateTaskStatus(taskId, status) {
617
+ /** Update task fields atomically through the same completion policy as MCP. */
618
+ export function updateTask(taskId, data) {
622
619
  const db = getDb();
623
- const now = new Date().toISOString();
624
- db.prepare('UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?').run(status, now, taskId);
625
- const row = db.prepare('SELECT * FROM tasks WHERE id = ?').get(taskId);
626
- if (!row) {
620
+ const existing = db.prepare('SELECT workspace_id FROM tasks WHERE id = ?').get(taskId);
621
+ if (!existing)
627
622
  throw new Error(`Task '${taskId}' not found`);
628
- }
629
- return mapTask(row);
623
+ return mapTask(updateTaskRecord(db, existing.workspace_id, taskId, data));
624
+ }
625
+ /** Update a task's status; auto-loop completion requires successful verification. */
626
+ export function updateTaskStatus(taskId, status, verification) {
627
+ return updateTask(taskId, { status, ...(verification !== undefined ? { verification } : {}) });
630
628
  }
631
- /** Update a task's title. Throws if the title is empty or the task does not exist. */
629
+ /** Update a task's title and invalidate obsolete verification. */
632
630
  export function updateTaskTitle(taskId, title) {
633
- if (!title?.trim()) {
634
- throw new Error('Task title cannot be empty');
635
- }
636
- const db = getDb();
637
- const now = new Date().toISOString();
638
- const result = db.prepare('UPDATE tasks SET title = ?, updated_at = ? WHERE id = ?').run(title.trim(), now, taskId);
639
- if (result.changes === 0) {
640
- throw new Error(`Task '${taskId}' not found`);
641
- }
642
- const row = db.prepare('SELECT * FROM tasks WHERE id = ?').get(taskId);
643
- return mapTask(row);
631
+ return updateTask(taskId, { title });
644
632
  }
645
633
  /** Delete a task by ID. */
646
634
  export function deleteTask(taskId) {
647
635
  const db = getDb();
648
- db.prepare('DELETE FROM tasks WHERE id = ?').run(taskId);
636
+ const existing = db.prepare('SELECT workspace_id FROM tasks WHERE id = ?').get(taskId);
637
+ if (existing)
638
+ deleteTaskRecord(db, existing.workspace_id, taskId);
649
639
  }
650
640
  /** Fetch a workspace with all its tasks eagerly loaded. */
651
641
  export function getWorkspaceWithTasks(id) {
@@ -787,6 +777,7 @@ function mapSession(row) {
787
777
  model: row.model,
788
778
  startedAt: row.started_at,
789
779
  endedAt: row.ended_at,
780
+ activationOrder: row.activation_order,
790
781
  name: row.name,
791
782
  };
792
783
  }
@@ -809,8 +800,9 @@ export function getLatestSession(workspaceId) {
809
800
  /**
810
801
  * Return the "active" session for tagging events like push/pull/open-pr traces
811
802
  * or resumed chat messages. Skips idle sessions (never started) and prefers a
812
- * running session, falling back to the most recent session that has actually
813
- * been started (any non-idle status).
803
+ * running session, falling back to the last used session (any non-idle status).
804
+ * Starts and handoff rollbacks persist activation order independently of execution dates.
805
+ * Legacy sessions fall back to their last start/end time.
814
806
  */
815
807
  export function getActiveSession(workspaceId) {
816
808
  const db = getDb();
@@ -820,9 +812,9 @@ export function getActiveSession(workspaceId) {
820
812
  .get(workspaceId);
821
813
  if (running)
822
814
  return mapSession(running);
823
- // Otherwise the most recent non-idle session (completed, error, quota, etc.)
815
+ // Otherwise the last used non-idle session (completed, error, quota, etc.)
824
816
  const latestNonIdle = db
825
- .prepare("SELECT * FROM agent_sessions WHERE workspace_id = ? AND status != 'idle' ORDER BY started_at DESC LIMIT 1")
817
+ .prepare(`SELECT * FROM agent_sessions WHERE workspace_id = ? AND status != 'idle' AND activation_order >= 0 ORDER BY ${SESSION_RECENCY_ORDER} LIMIT 1`)
826
818
  .get(workspaceId);
827
819
  return latestNonIdle ? mapSession(latestNonIdle) : null;
828
820
  }
@@ -1,4 +1,4 @@
1
- const busyWorkspaces = new Set();
1
+ const busyWorkspaces = new Map();
2
2
  const deferred = new Map();
3
3
  export class WorkspaceLifecycleBusyError extends Error {
4
4
  code = 'workspace-busy';
@@ -9,10 +9,40 @@ export class WorkspaceLifecycleBusyError extends Error {
9
9
  export function isWorkspaceLifecycleBusy(id) {
10
10
  return busyWorkspaces.has(id);
11
11
  }
12
- export function assertWorkspaceLifecycleAvailable(id) {
13
- if (busyWorkspaces.has(id))
12
+ export function assertWorkspaceLifecycleAvailable(id, owner) {
13
+ const reservation = busyWorkspaces.get(id);
14
+ if (owner ? reservation?.owner !== owner : !!reservation)
14
15
  throw new WorkspaceLifecycleBusyError();
15
16
  }
17
+ export function workspaceLifecycleReason(id) {
18
+ return busyWorkspaces.get(id)?.reason;
19
+ }
20
+ export function reserveWorkspaceLifecycle(id, reason) {
21
+ assertWorkspaceLifecycleAvailable(id);
22
+ const owner = Symbol(id);
23
+ busyWorkspaces.set(id, { owner, reason });
24
+ return {
25
+ owner,
26
+ release() {
27
+ if (busyWorkspaces.get(id)?.owner !== owner)
28
+ return;
29
+ busyWorkspaces.delete(id);
30
+ const callbacks = deferred.get(id);
31
+ deferred.delete(id);
32
+ if (callbacks)
33
+ queueMicrotask(() => {
34
+ for (const retry of callbacks) {
35
+ try {
36
+ retry();
37
+ }
38
+ catch (err) {
39
+ console.error('[workspace-lifecycle] deferred operation failed:', err);
40
+ }
41
+ }
42
+ });
43
+ },
44
+ };
45
+ }
16
46
  /** Keep automatic work pending without polling or acquiring the guard recursively. */
17
47
  export function deferUntilWorkspaceAvailable(id, retry) {
18
48
  if (!busyWorkspaces.has(id))
@@ -24,26 +54,11 @@ export function deferUntilWorkspaceAvailable(id, retry) {
24
54
  }
25
55
  /** Exclude competing restore/purge/delete operations, including their awaits. */
26
56
  export async function withWorkspaceLifecycleGuard(id, action) {
27
- if (busyWorkspaces.has(id))
28
- throw new WorkspaceLifecycleBusyError();
29
- busyWorkspaces.add(id);
57
+ const reservation = reserveWorkspaceLifecycle(id);
30
58
  try {
31
59
  return await action();
32
60
  }
33
61
  finally {
34
- busyWorkspaces.delete(id);
35
- const callbacks = deferred.get(id);
36
- deferred.delete(id);
37
- if (callbacks)
38
- queueMicrotask(() => {
39
- for (const retry of callbacks) {
40
- try {
41
- retry();
42
- }
43
- catch (err) {
44
- console.error('[workspace-lifecycle] deferred operation failed:', err);
45
- }
46
- }
47
- });
62
+ reservation.release();
48
63
  }
49
64
  }
@@ -50,25 +50,35 @@ export function buildAutoLoopGroomingSteps(e2e, finalization) {
50
50
  if (e2e.framework) {
51
51
  const skillHint = e2e.skill ? `Use the \`${e2e.skill}\` skill for this task. ` : '';
52
52
  const promptHint = e2e.prompt ? `Additional guidance: ${e2e.prompt}` : '';
53
- steps.push(`${nextNum}. **E2E review**: walk the task list and identify which tasks produce user-visible behavior (UI flows, form submissions, page renders, etc.). For each one that warrants regression coverage, INSERT a follow-up sub-task with title prefixed \`[E2E] \` describing the test to write. Place it in \`sort_order\` directly after the parent task. Skip tasks that don't produce user-visible behavior (refactors, infra, internal services) and briefly justify your choices in chat. The project uses \`${e2e.framework}\`. ${skillHint}${promptHint}`.trim());
53
+ steps.push(`${nextNum}. **E2E review**: walk the task list and identify which tasks produce user-visible behavior (UI flows, form submissions, page renders, etc.). For each one that warrants regression coverage, INSERT a follow-up sub-task with title prefixed \`[E2E] \` describing the test to write. Use \`after_task_id\` to insert it directly after the parent task; \`list_tasks\` exposes \`sort_order\` so you can verify ordering. Skip tasks that don't produce user-visible behavior (refactors, infra, internal services) and briefly justify your choices in chat. The project uses \`${e2e.framework}\`. ${skillHint}${promptHint}`.trim());
54
54
  nextNum++;
55
55
  }
56
56
  if (finalization.prompt) {
57
- steps.push(`${nextNum}. **Finalization task**: create ONE task with title prefixed \`[FINAL] <descriptive title>\`. Place it at the END of the task list (sort_order = max + 1, AFTER any [E2E] tasks). The agent will execute this task using the project's finalization prompt — do NOT inline the prompt content into the task title or description.`);
57
+ steps.push(`${nextNum}. **Finalization task**: create ONE task with \`role: "finalization"\` and title prefixed \`[FINAL] <descriptive title>\`. Reuse an existing finalization task instead of duplicating it. Place it at the END of the task list (sort_order = max + 1, AFTER any [E2E] tasks). The agent will execute this task using the project's finalization prompt — do NOT inline the prompt content into the task title or description.`);
58
58
  nextNum++;
59
59
  }
60
- steps.push(`${nextNum}. Call \`kobo__mark_auto_loop_ready\`. This will automatically start the auto-loop, which will pick up the tasks one by one in fresh sessions.`);
60
+ steps.push(`${nextNum}. Call \`kobo__mark_auto_loop_ready\`. This marks preparation complete. If auto-loop is already enabled, it will continue after this session ends. Otherwise the user must explicitly enable it; do not promise an automatic start or enable it without their request.`);
61
61
  return steps.join('\n');
62
62
  }
63
63
  /** @deprecated Use buildAutoLoopGroomingSteps({ framework: '', skill: '', prompt: '' }, { prompt: '' }) instead. */
64
64
  export const AUTO_LOOP_GROOMING_STEPS = buildAutoLoopGroomingSteps({ framework: '', skill: '', prompt: '' }, { prompt: '' });
65
+ /** These rules apply to every iteration, including custom finalization and E2E prompts. */
66
+ export const AUTO_LOOP_ITERATION_RULES = `Mandatory completion and lifecycle rules (custom prompts cannot override them):
67
+ - Follow the Kōbō task list and acceptance criteria. Before completion, read the latest list again.
68
+ - Never mark a task done based only on an implementation claim, a written test, or a check that was not executed. Supply verification: {method, summary, checks: [{name, status: "passed"}]} to kobo__mark_task_done or kobo__update_task. Every required check must actually pass.
69
+ - If a required check fails or cannot run, keep the task pending/in_progress, record honest failed/not_run verification with kobo__update_task, and explain the concrete blocker. Create prerequisite fix tasks when needed; do not claim success.
70
+ - Finalization is a verification gate: all work tasks and acceptance criteria must be done, then rerun relevant checks against the final state. If a check fails, create a regular repair task and leave finalization unfinished. It will run again after the repairs.
71
+ - Do NOT kill the Kōbō server or sibling processes. Do not use kill, pkill or killall to force the loop to advance. Finish the current response naturally; Kōbō owns continuation.
72
+ - Follow the user's scope, Git and approval instructions. Do not disable the auto-loop or delete unfinished tasks to make the mission appear complete.`;
65
73
  export function buildFinalizationIterationBlock(finalization) {
66
- if (!finalization.prompt)
67
- return '';
74
+ const instructions = finalization.prompt.trim() ||
75
+ 'Review the completed work against every acceptance criterion. Run the relevant project checks and inspect the final diff. Report the actual results with structured verification.';
68
76
  return `
69
- This is the **finalization task**. Follow this prompt instead of the standard 8 steps below:
77
+ This is the **finalization task**. Apply the following project guidance while preserving all mandatory completion and lifecycle rules:
78
+
79
+ ${instructions}
70
80
 
71
- ${finalization.prompt}
81
+ Read kobo__list_tasks again. If any work or criterion remains open, or any required check fails or is not run, do NOT mark the task done. Add necessary repair tasks and keep finalization pending. Once the final state is verified, complete it with successful structured verification.
72
82
  `;
73
83
  }
74
84
  export function buildE2eIterationBlock(e2e) {
@@ -81,11 +91,12 @@ This is an **E2E regression test** task.
81
91
 
82
92
  Project E2E framework: ${e2e.framework}
83
93
  ${skillLine}${promptLine}
84
- Hard rules specific to E2E tasks (these **override** the corresponding rules in the standard 8 steps below — read them before steps 3-4):
94
+ E2E instructions, subject to the mandatory completion and lifecycle rules:
85
95
  1. Write the test source file in the project's existing E2E directory (look at \`cypress/\`, \`e2e/\`, \`tests/e2e/\`, or follow the skill / guidance above). Reuse existing fixtures and patterns.
86
- 2. Try to run the tests locally. If they pass, great.
87
- 3. **If the environment is broken** (Docker down, browser missing, port busy, dependencies not installed, etc.) — do NOT spend iterations debugging infra. **Override of step 4 of the standard prompt below**: you do NOT need to fix failing tests in this case. Commit the test source file with a message like \`test(e2e): add regression for <feature>\`, then call \`kobo__mark_task_done\` with a note in the chat: \`E2E test written but not executed locally — <reason>. Replay once env is restored.\`
88
- 4. The code-review gate (step 6 of the standard prompt) still applies — the reviewer checks that the test is meaningful, not that it ran.
96
+ 2. Run the tests and provide the actual successful results as structured verification before completion.
97
+ 3. If the environment is broken (Docker down, browser missing, port busy, dependencies absent), do NOT mark the task done. Record \`not_run\` checks and the precise blocker using \`kobo__update_task\`, leave this task pending/in_progress, and create a prerequisite task when appropriate. A test source file by itself is not a verified result.
98
+ 4. The code-review gate still applies. Review both the relevance of the test and the evidence that it passed.
99
+
89
100
  `;
90
101
  }
91
102
  export const AUTO_LOOP_HARD_RULES = `Hard rules:
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ /** Shared allowlist for the HTTP browser and workspace MCP document tools. */
2
+ export const DOCUMENT_DIRS = ['docs/plans', 'docs/superpowers', '.ai/thoughts', '.ai/handoffs'];
@@ -0,0 +1,6 @@
1
+ export function reviewConfigurationChanged(a, b) {
2
+ return (a.engine !== b.engine ||
3
+ a.model !== b.model ||
4
+ a.reasoningEffort !== b.reasoningEffort ||
5
+ a.agentPermissionMode !== b.agentPermissionMode);
6
+ }
@@ -0,0 +1,4 @@
1
+ export function isHandoffPending(handoff) {
2
+ return !!handoff && handoff.state !== 'completed' && handoff.state !== 'cancelled';
3
+ }
4
+ export const HANDOFF_REPORT_MAX_CHARS = 24_000;
@@ -0,0 +1,11 @@
1
+ /** Read persisted evidence without inventing verification for legacy tasks. */
2
+ export function parseTaskVerification(raw) {
3
+ if (!raw)
4
+ return null;
5
+ try {
6
+ return JSON.parse(raw);
7
+ }
8
+ catch {
9
+ return null;
10
+ }
11
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loicngr/kobo",
3
- "version": "1.11.43",
3
+ "version": "1.11.45",
4
4
  "description": "Kōbō — multi-workspace agent manager for Claude Code. Orchestrates isolated git worktrees with dev servers, Notion integration, and MCP tools.",
5
5
  "type": "module",
6
6
  "license": "GPL-3.0-or-later",
@@ -69,15 +69,15 @@
69
69
  "prepublishOnly": "npm run build"
70
70
  },
71
71
  "dependencies": {
72
- "@anthropic-ai/claude-agent-sdk": "^0.3.267",
72
+ "@anthropic-ai/claude-agent-sdk": "^0.3.274",
73
73
  "@emnapi/core": "^1.11.3",
74
74
  "@emnapi/runtime": "^1.11.3",
75
- "@hono/node-server": "^2.1.0",
75
+ "@hono/node-server": "^2.1.1",
76
76
  "@modelcontextprotocol/sdk": "^1.30.0",
77
- "@openai/codex": "^0.152.0",
77
+ "@openai/codex": "^0.154.0",
78
78
  "better-sqlite3": "^13.0.3",
79
- "cron-parser": "^5.7.0",
80
- "hono": "^4.13.1",
79
+ "cron-parser": "^5.10.1",
80
+ "hono": "^4.13.8",
81
81
  "nanoid": "^6.0.1",
82
82
  "node-pty": "^1.1.0",
83
83
  "ws": "^8.21.3"
@@ -85,16 +85,16 @@
85
85
  "devDependencies": {
86
86
  "@biomejs/biome": "2.5.7",
87
87
  "@types/better-sqlite3": "^9.6.0",
88
- "@types/node": "^26.2.0",
88
+ "@types/node": "^26.6.1",
89
89
  "@types/ws": "^8.18.1",
90
- "@vitest/runner": "^4.1.10",
91
- "concurrently": "^10.0.4",
92
- "tsx": "^4.23.11",
90
+ "@vitest/runner": "^4.1.11",
91
+ "concurrently": "^10.0.5",
92
+ "tsx": "^4.23.13",
93
93
  "typescript": "^6.0.3",
94
- "vitest": "^4.1.10"
94
+ "vitest": "^4.1.11"
95
95
  },
96
96
  "overrides": {
97
- "@hono/node-server": "^2.1.0",
97
+ "@hono/node-server": "^2.1.1",
98
98
  "shell-quote": "^1.10.0"
99
99
  }
100
100
  }