@loicngr/kobo 2.1.29 → 2.1.30

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 (160) hide show
  1. package/AGENTS.md +59 -10
  2. package/CHANGELOG.md +16 -1
  3. package/CONFIGURATION.md +114 -14
  4. package/CONTRIBUTING.md +1 -1
  5. package/DESIGN.md +1 -1
  6. package/README.md +12 -9
  7. package/SECURITY.md +7 -2
  8. package/dist/server/db/memory-schema.js +5 -5
  9. package/dist/server/db/migrations.js +2 -0
  10. package/dist/server/db/mistral-memory-migration.js +40 -0
  11. package/dist/server/routes/engines.js +14 -0
  12. package/dist/server/routes/environment.js +1 -1
  13. package/dist/server/routes/health.js +14 -3
  14. package/dist/server/routes/settings.js +7 -1
  15. package/dist/server/routes/workspaces.js +116 -9
  16. package/dist/server/services/agent/engines/claude-code/capabilities.js +2 -0
  17. package/dist/server/services/agent/engines/claude-code/engine.js +3 -0
  18. package/dist/server/services/agent/engines/codex/capabilities.js +2 -0
  19. package/dist/server/services/agent/engines/codex/engine.js +3 -0
  20. package/dist/server/services/agent/engines/configuration.js +14 -0
  21. package/dist/server/services/agent/engines/mistral-vibe/capabilities.js +13 -0
  22. package/dist/server/services/agent/engines/mistral-vibe/client.js +64 -0
  23. package/dist/server/services/agent/engines/mistral-vibe/configuration.js +76 -0
  24. package/dist/server/services/agent/engines/mistral-vibe/engine.js +361 -0
  25. package/dist/server/services/agent/engines/mistral-vibe/event-mapper.js +297 -0
  26. package/dist/server/services/agent/engines/mistral-vibe/options-builder.js +35 -0
  27. package/dist/server/services/agent/engines/mistral-vibe/server-requests.js +278 -0
  28. package/dist/server/services/agent/engines/mistral-vibe/spawn.js +176 -0
  29. package/dist/server/services/agent/engines/registry.js +2 -0
  30. package/dist/server/services/agent/engines/types.js +6 -0
  31. package/dist/server/services/agent/orchestrator.js +88 -25
  32. package/dist/server/services/agent/session-controller.js +5 -0
  33. package/dist/server/services/auto-loop-final-review-service.js +10 -3
  34. package/dist/server/services/environment-check-service.js +6 -1
  35. package/dist/server/services/memory-service.js +1 -1
  36. package/dist/server/services/mistral-options-service.js +145 -0
  37. package/dist/server/services/pr-watcher-service.js +40 -11
  38. package/dist/server/services/review-service.js +33 -9
  39. package/dist/server/services/session-handoff-service.js +28 -2
  40. package/dist/server/services/settings-defaults.js +1 -0
  41. package/dist/server/services/settings-service.js +19 -1
  42. package/dist/server/services/workspace-creation-mcp-service.js +50 -8
  43. package/dist/server/services/workspace-dialogue-service.js +2 -0
  44. package/dist/server/services/workspace-message-service.js +19 -1
  45. package/dist/server/services/workspace-permission-policy-service.js +24 -0
  46. package/dist/server/utils/bounded-process.js +1 -1
  47. package/dist/shared/launch-instructions.js +18 -7
  48. package/dist/shared/memory.js +1 -1
  49. package/dist/shared/mistral-models.js +9 -0
  50. package/dist/shared/mistral-options.js +1 -0
  51. package/dist/shared/question-selections.js +16 -0
  52. package/dist/shared/workspace-creation-tools.js +8 -2
  53. package/dist/shared/workspace-dialogue-tools.js +15 -4
  54. package/docs/deployment-vps.md +10 -3
  55. package/docs/getting-started.md +3 -2
  56. package/docs/troubleshooting.md +1 -1
  57. package/package.json +7 -1
  58. package/src/client/dist/pwa/assets/ActivityFeed-C5stmHXP.js +2 -0
  59. package/src/client/dist/pwa/assets/AutoLoopFinalReviewSettings-CIDSX32d.css +1 -0
  60. package/src/client/dist/pwa/assets/AutoLoopFinalReviewSettings-DG32W_0G.js +1 -0
  61. package/src/client/dist/pwa/assets/{ChangelogPage-C5KjAOMG.js → ChangelogPage-_vItY8vA.js} +1 -1
  62. package/src/client/dist/pwa/assets/{ComparisonPanel-XXyzPEzW.js → ComparisonPanel-B3BMzQH0.js} +1 -1
  63. package/src/client/dist/pwa/assets/CreatePage-FlGrfmjX.js +2 -0
  64. package/src/client/dist/pwa/assets/CreatePage-oh8ihVC4.css +1 -0
  65. package/src/client/dist/pwa/assets/{DashboardPage-DyhUu26C.js → DashboardPage-lJGwWQYQ.js} +1 -1
  66. package/src/client/dist/pwa/assets/DiffViewer-DIaQH3lA.js +8 -0
  67. package/src/client/dist/pwa/assets/{DocumentsPanel-ItndGxeB.js → DocumentsPanel-Ct-nm4fX.js} +1 -1
  68. package/src/client/dist/pwa/assets/{DrawerToggleButton-DfzS_CD8.js → DrawerToggleButton-M5qv4Io-.js} +1 -1
  69. package/src/client/dist/pwa/assets/GitPanel-BaHCgb3p.js +3 -0
  70. package/src/client/dist/pwa/assets/GitPanel-C8vw2qvW.css +1 -0
  71. package/src/client/dist/pwa/assets/HealthPage-DR3wa6iD.js +1 -0
  72. package/src/client/dist/pwa/assets/HealthPage-Dbn4J_DT.css +1 -0
  73. package/src/client/dist/pwa/assets/{MainLayout-DoAEgANU.css → MainLayout-CHi8qP1D.css} +1 -1
  74. package/src/client/dist/pwa/assets/MainLayout-WJabICsA.js +2 -0
  75. package/src/client/dist/pwa/assets/{MemoryPanel-CZzI7_nY.js → MemoryPanel-CRpWdfc0.js} +1 -1
  76. package/src/client/dist/pwa/assets/{MemoryProposalList-DqNTycY1.js → MemoryProposalList-DGz3lDbL.js} +1 -1
  77. package/src/client/dist/pwa/assets/{QBtnDropdown-D7ml7zHo.js → QBtnDropdown-tt8rUNNR.js} +1 -1
  78. package/src/client/dist/pwa/assets/{QExpansionItem-BVb067oc.js → QExpansionItem-BkrI3UPL.js} +1 -1
  79. package/src/client/dist/pwa/assets/{QScrollArea-CUDWkEHh.js → QScrollArea-BvgnY-oA.js} +1 -1
  80. package/src/client/dist/pwa/assets/QSelect-BtZB1wrB.js +1 -0
  81. package/src/client/dist/pwa/assets/{QTooltip-B5QJIU3p.js → QTooltip-CDH6p745.js} +1 -1
  82. package/src/client/dist/pwa/assets/{SchedulePanel-IJOcrHgk.js → SchedulePanel-C2rWlh7l.js} +1 -1
  83. package/src/client/dist/pwa/assets/{SearchPage-BpTRWtne.js → SearchPage-BFd4NQwJ.js} +1 -1
  84. package/src/client/dist/pwa/assets/{SettingsPage-B4rmd2WX.css → SettingsPage-Bm0TcarK.css} +1 -1
  85. package/src/client/dist/pwa/assets/SettingsPage-DDQCgnjL.js +9 -0
  86. package/src/client/dist/pwa/assets/{SplitWorkspacePage-BZ8wN4ii.js → SplitWorkspacePage-DKCtOZO4.js} +1 -1
  87. package/src/client/dist/pwa/assets/{TerminalPanel-pY-RGBb6.js → TerminalPanel-BE_MSZF0.js} +4 -4
  88. package/src/client/dist/pwa/assets/{ToolCallItem-C9ksVseJ.js → ToolCallItem-BEYaOtgh.js} +3 -3
  89. package/src/client/dist/pwa/assets/{TouchPan-DRynDIve.js → TouchPan-DeYoOjX5.js} +1 -1
  90. package/src/client/dist/pwa/assets/{TourReplayButton-D4h1QR6C.js → TourReplayButton-D5lMwIKt.js} +1 -1
  91. package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-BHEAsIoo.js +1 -0
  92. package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-v7ein__H.css +1 -0
  93. package/src/client/dist/pwa/assets/WorkspacePage-5_wsJPaJ.js +4 -0
  94. package/src/client/dist/pwa/assets/WorkspacePage-CtUXz0Cq.css +1 -0
  95. package/src/client/dist/pwa/assets/{browser-BcOi2ES2.js → browser-ogy_9LVv.js} +1 -1
  96. package/src/client/dist/pwa/assets/{build-path-tree-mDco3pi7.js → build-path-tree-CRXxaYkA.js} +1 -1
  97. package/src/client/dist/pwa/assets/custom-sounds-iLTkCd_i.js +1 -0
  98. package/src/client/dist/pwa/assets/de-BqDU7iZH.js +17 -0
  99. package/src/client/dist/pwa/assets/{editor.api-CRrihXg2.js → editor.api-Dx6TbfZG.js} +1 -1
  100. package/src/client/dist/pwa/assets/{efforts-C-p9DzkL.js → efforts-DnJh2fW_.js} +2 -2
  101. package/src/client/dist/pwa/assets/es-D4VMXQRk.js +17 -0
  102. package/src/client/dist/pwa/assets/{expand-template-gIwrPbeS.js → expand-template-PUEVWny3.js} +1 -1
  103. package/src/client/dist/pwa/assets/{format-DeBnwmgW.js → format-yf9XM3dV.js} +1 -1
  104. package/src/client/dist/pwa/assets/{formatters-CxZZ_WTb.js → formatters-D0sXu9Oe.js} +1 -1
  105. package/src/client/dist/pwa/assets/fr-DIyaCFB3.js +17 -0
  106. package/src/client/dist/pwa/assets/{freemarker2-h7OLlqTn.js → freemarker2-l_xIHxRf.js} +1 -1
  107. package/src/client/dist/pwa/assets/{handlebars-BCWmi1-q.js → handlebars-CYgq99yQ.js} +1 -1
  108. package/src/client/dist/pwa/assets/{html-MUMpfUBd.js → html-C28GatN_.js} +1 -1
  109. package/src/client/dist/pwa/assets/i18n-2AmHIUy0.js +1 -0
  110. package/src/client/dist/pwa/assets/index-MMYMztJ_.js +20 -0
  111. package/src/client/dist/pwa/assets/it-CF6_4OS0.js +17 -0
  112. package/src/client/dist/pwa/assets/{javascript-CHDwijEy.js → javascript-YCSueyyk.js} +1 -1
  113. package/src/client/dist/pwa/assets/{liquid-dFrnLevt.js → liquid-CUBn90Gs.js} +1 -1
  114. package/src/client/dist/pwa/assets/{mdx-BHS7JVD4.js → mdx-C5WOHShJ.js} +1 -1
  115. package/src/client/dist/pwa/assets/{monaco-contributions-DCWZe7nX.js → monaco-contributions-DYwqbh7m.js} +1 -1
  116. package/src/client/dist/pwa/assets/{monaco.contribution-Dx0gcAn4.js → monaco.contribution-CoFzrPQl.js} +2 -2
  117. package/src/client/dist/pwa/assets/network-auth-BpJ6Noax.js +1 -0
  118. package/src/client/dist/pwa/assets/notify-theme-NPq4-Tup.js +1 -0
  119. package/src/client/dist/pwa/assets/permissionModes-Bbl901Hx.js +36 -0
  120. package/src/client/dist/pwa/assets/{project-color-DihsE4yN.js → project-color-gNyhKJlV.js} +1 -1
  121. package/src/client/dist/pwa/assets/{python-BtjZnK9y.js → python-lhKUZdPj.js} +1 -1
  122. package/src/client/dist/pwa/assets/{razor-DBcOGz0a.js → razor-Chi44rEL.js} +1 -1
  123. package/src/client/dist/pwa/assets/{typescript-B320c7wU.js → typescript-CU2TXVm6.js} +1 -1
  124. package/src/client/dist/pwa/assets/{use-search-index-status-B95GPaJP.js → use-search-index-status-CQ9S05G7.js} +1 -1
  125. package/src/client/dist/pwa/assets/{use-tours-C_5R1Y5D.js → use-tours-BmrZbHjz.js} +2 -2
  126. package/src/client/dist/pwa/assets/use-worktree-restore-DcucRlLh.js +1 -0
  127. package/src/client/dist/pwa/assets/{workspace-templates-B9UETKER.js → workspace-templates-DaG42EEt.js} +1 -1
  128. package/src/client/dist/pwa/assets/{xml-uK8LK4OB.js → xml-CPO5LI6F.js} +1 -1
  129. package/src/client/dist/pwa/assets/{yaml-B2D1l_tz.js → yaml-CoYs82jI.js} +1 -1
  130. package/src/client/dist/pwa/index.html +1 -1
  131. package/src/client/dist/pwa/sw.js +1 -1
  132. package/src/mcp-server/README.md +18 -6
  133. package/src/client/dist/pwa/assets/ActivityFeed-CiTe2VUQ.js +0 -2
  134. package/src/client/dist/pwa/assets/AutoLoopFinalReviewSettings-Q-OlSQ1c.css +0 -1
  135. package/src/client/dist/pwa/assets/AutoLoopFinalReviewSettings-v3jXoH_Q.js +0 -1
  136. package/src/client/dist/pwa/assets/CreatePage-DxOes_cF.css +0 -1
  137. package/src/client/dist/pwa/assets/CreatePage-MYCexRJu.js +0 -2
  138. package/src/client/dist/pwa/assets/DiffViewer-CvsmC9d5.js +0 -8
  139. package/src/client/dist/pwa/assets/GitPanel-C1ybi9bZ.js +0 -3
  140. package/src/client/dist/pwa/assets/GitPanel-yHvU38dp.css +0 -1
  141. package/src/client/dist/pwa/assets/HealthPage-C1u35ANL.js +0 -1
  142. package/src/client/dist/pwa/assets/HealthPage-Cnlhrn--.css +0 -1
  143. package/src/client/dist/pwa/assets/MainLayout-BEAGPqei.js +0 -2
  144. package/src/client/dist/pwa/assets/QSelect-DOnv_ry0.js +0 -1
  145. package/src/client/dist/pwa/assets/SettingsPage-CFaRN9Pk.js +0 -9
  146. package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-B9LE8HtU.css +0 -1
  147. package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-Bkj2DhOn.js +0 -1
  148. package/src/client/dist/pwa/assets/WorkspacePage-BEBwwr81.css +0 -1
  149. package/src/client/dist/pwa/assets/WorkspacePage-BTJnap9a.js +0 -4
  150. package/src/client/dist/pwa/assets/custom-sounds-T5HKzrWh.js +0 -1
  151. package/src/client/dist/pwa/assets/de-suqWUSXa.js +0 -17
  152. package/src/client/dist/pwa/assets/es-Dcptbz5U.js +0 -17
  153. package/src/client/dist/pwa/assets/fr-Db70bqVq.js +0 -17
  154. package/src/client/dist/pwa/assets/i18n-DLprfNLs.js +0 -1
  155. package/src/client/dist/pwa/assets/index-DLoUeovT.js +0 -20
  156. package/src/client/dist/pwa/assets/it-Cloo58Nn.js +0 -17
  157. package/src/client/dist/pwa/assets/network-auth-DDMAzwYH.js +0 -1
  158. package/src/client/dist/pwa/assets/notify-theme-JuVT96rq.js +0 -1
  159. package/src/client/dist/pwa/assets/permissionModes-D-Nexnoo.js +0 -36
  160. package/src/client/dist/pwa/assets/use-worktree-restore-Cj8eGE9s.js +0 -1
@@ -0,0 +1,40 @@
1
+ /** Rebuild only engine CHECK constraints; the migration runner owns the transaction. */
2
+ export function migrateMistralMemoryEngine(db) {
3
+ // Parents precede children so replacement FKs never point at tables being dropped.
4
+ const tables = [
5
+ 'memory_entries',
6
+ 'memory_budget_contexts',
7
+ 'memory_proposals',
8
+ 'memory_operations',
9
+ 'memory_contexts',
10
+ ];
11
+ const definitions = tables.map((name) => ({
12
+ name,
13
+ sql: db.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?").get(name)
14
+ .sql,
15
+ }));
16
+ if (definitions.every(({ sql }) => sql.includes("'mistral-vibe'")))
17
+ return;
18
+ const indexes = tables.flatMap((name) => db
19
+ .prepare("SELECT sql FROM sqlite_master WHERE type = 'index' AND tbl_name = ? AND sql IS NOT NULL")
20
+ .all(name));
21
+ const sequence = db
22
+ .prepare("SELECT seq FROM sqlite_sequence WHERE name = 'memory_operations'")
23
+ .safeIntegers()
24
+ .get();
25
+ for (const { name, sql } of definitions) {
26
+ let replacement = sql.replaceAll("'claude-code', 'codex'", "'claude-code', 'codex', 'mistral-vibe'");
27
+ for (const table of tables)
28
+ replacement = replacement.replaceAll(new RegExp(`\\b${table}\\b`, 'g'), `${table}_mistral`);
29
+ db.exec(replacement);
30
+ db.exec(`INSERT INTO ${name}_mistral SELECT * FROM ${name}`);
31
+ }
32
+ for (const name of [...tables].reverse())
33
+ db.exec(`DROP TABLE ${name}`);
34
+ for (const name of tables)
35
+ db.exec(`ALTER TABLE ${name}_mistral RENAME TO ${name}`);
36
+ for (const { sql } of indexes)
37
+ db.exec(sql);
38
+ if (sequence)
39
+ db.prepare("UPDATE sqlite_sequence SET seq = ? WHERE name = 'memory_operations'").run(sequence.seq);
40
+ }
@@ -1,5 +1,6 @@
1
1
  import { Hono } from 'hono';
2
2
  import { listEngines } from '../services/agent/engines/registry.js';
3
+ import { getMistralOptions } from '../services/mistral-options-service.js';
3
4
  /** Hono sub-router exposing `GET /` — the list of registered agent engines with capabilities. */
4
5
  export const enginesRouter = new Hono();
5
6
  enginesRouter.get('/', (c) => c.json(listEngines().map((e) => ({
@@ -7,3 +8,16 @@ enginesRouter.get('/', (c) => c.json(listEngines().map((e) => ({
7
8
  displayName: e.displayName,
8
9
  capabilities: e.capabilities,
9
10
  }))));
11
+ enginesRouter.get('/mistral-vibe/options', async (c) => {
12
+ try {
13
+ return c.json(await getMistralOptions({
14
+ workspaceId: c.req.query('workspaceId'),
15
+ projectPath: c.req.query('projectPath'),
16
+ model: c.req.query('model'),
17
+ agentPermissionMode: c.req.query('agentPermissionMode'),
18
+ }));
19
+ }
20
+ catch (error) {
21
+ return c.json({ error: error instanceof Error ? error.message : 'Native Vibe options unavailable' }, 400);
22
+ }
23
+ });
@@ -4,7 +4,7 @@ const app = new Hono();
4
4
  app.get('/', async (c) => {
5
5
  const engine = c.req.query('engine') ?? 'claude-code';
6
6
  const projectPath = c.req.query('projectPath')?.trim();
7
- if (!['claude-code', 'codex'].includes(engine) ||
7
+ if (!['claude-code', 'codex', 'mistral-vibe'].includes(engine) ||
8
8
  (projectPath && (projectPath.length > 4096 || projectPath.includes('\0')))) {
9
9
  return c.json({ error: 'Invalid environment check request' }, 400);
10
10
  }
@@ -23,7 +23,11 @@ function integrationConfigured(integration, key) {
23
23
  }
24
24
  async function checkCli(binary) {
25
25
  try {
26
- const { stdout } = await execFileAsync(binary, ['--version'], { encoding: 'utf-8', timeout: 2_000 });
26
+ const { stdout } = await execFileAsync(binary, ['--version'], {
27
+ encoding: 'utf-8',
28
+ timeout: 2_000,
29
+ maxBuffer: 64 * 1024,
30
+ });
27
31
  return { available: true, version: stdout.trim() || null };
28
32
  }
29
33
  catch {
@@ -78,7 +82,13 @@ app.get('/report', async (c) => {
78
82
  const db = getDb();
79
83
  const dbPath = getDbPath();
80
84
  const home = getKoboHome();
81
- const cliChecks = Promise.all([checkClaudeCli(), checkCodexCli(), checkCli('gh'), checkCli('glab')]);
85
+ const cliChecks = Promise.all([
86
+ checkClaudeCli(),
87
+ checkCodexCli(),
88
+ checkCli('vibe-acp'),
89
+ checkCli('gh'),
90
+ checkCli('glab'),
91
+ ]);
82
92
  // DB schema version
83
93
  const row = db.prepare('SELECT MAX(version) as v FROM schema_migrations').get();
84
94
  const dbSchemaVersion = row?.v ?? 0;
@@ -170,7 +180,7 @@ app.get('/report', async (c) => {
170
180
  .all();
171
181
  const settingsRow = db.prepare('SELECT COUNT(*) as n FROM workspaces').get();
172
182
  const archivedRow = db.prepare('SELECT COUNT(*) as n FROM workspaces WHERE archived_at IS NOT NULL').get();
173
- const [claudeCli, codexCli, ghCli, glabCli] = await cliChecks;
183
+ const [claudeCli, codexCli, mistralCli, ghCli, glabCli] = await cliChecks;
174
184
  const report = {
175
185
  version: getPackageVersion(),
176
186
  koboHome: home,
@@ -183,6 +193,7 @@ app.get('/report', async (c) => {
183
193
  settings: { schemaVersion: SETTINGS_SCHEMA_VERSION },
184
194
  claudeCli,
185
195
  codexCli,
196
+ mistralCli,
186
197
  forgeCli: { gh: ghCli, glab: glabCli },
187
198
  workspaces: {
188
199
  total: settingsRow.n,
@@ -3,7 +3,7 @@ import { isMemoryMode } from '../../shared/memory.js';
3
3
  import { isValidSkillSuite } from '../../shared/skill-suite-prompts.js';
4
4
  import { isWorkflowPolicy } from '../../shared/workflow-policy.js';
5
5
  import { getDb } from '../db/index.js';
6
- import { getBackendPort } from '../services/agent/orchestrator.js';
6
+ import { getBackendPort, notifyCapacityAvailable } from '../services/agent/orchestrator.js';
7
7
  import { DEFAULT_NOTION_INITIAL_PROMPT, DEFAULT_SENTRY_INITIAL_PROMPT, } from '../services/initial-prompt-template-service.js';
8
8
  import { getMcpConnectionInfo } from '../services/mcp-connection-info-service.js';
9
9
  import { generateToken, getLanUrls } from '../services/network-access-service.js';
@@ -167,7 +167,13 @@ app.put('/global', async (c) => {
167
167
  return c.json({ error: 'Invalid workflowPolicy' }, 400);
168
168
  if (body.memoryMode !== undefined && !isMemoryMode(body.memoryMode))
169
169
  return c.json({ error: 'Invalid memoryMode' }, 400);
170
+ if ('maxConcurrentAgents' in body &&
171
+ (!Number.isSafeInteger(body.maxConcurrentAgents) || body.maxConcurrentAgents < 0))
172
+ return c.json({ error: 'maxConcurrentAgents must be a non-negative safe integer' }, 400);
173
+ const previousLimit = 'maxConcurrentAgents' in body ? settingsService.getGlobalSettings().maxConcurrentAgents : undefined;
170
174
  const updated = settingsService.updateGlobalSettings(body);
175
+ if (previousLimit && (updated.maxConcurrentAgents === 0 || updated.maxConcurrentAgents > previousLimit))
176
+ notifyCapacityAvailable();
171
177
  // The client assigns this response straight into its store, so echoing the
172
178
  // real credentials back would undo the masking on GET the first time
173
179
  // anyone saves anything.
@@ -4,9 +4,10 @@ import { nanoid } from 'nanoid';
4
4
  import { isMergeMethod } from '../../shared/forge-merge.js';
5
5
  import { isWorkflowPolicy, resolveWorkflowPolicy, WORKFLOW_ACTIONS, } from '../../shared/workflow-policy.js';
6
6
  import * as finalReviewService from '../services/auto-loop-final-review-service.js';
7
+ import { preflightMistralConfiguration } from '../services/mistral-options-service.js';
7
8
  import { getReviewReturnStatus } from '../services/review-return-service.js';
8
9
  import { getSearchIndexStatus, searchEvents } from '../services/search-service.js';
9
- import { createSessionHandoff, decideSessionHandoff, getCurrentSessionHandoff, SessionHandoffError, submitSessionHandoff, waitForSessionHandoff, } from '../services/session-handoff-service.js';
10
+ import { createSessionHandoff, decideSessionHandoff, getCurrentSessionHandoff, preflightSessionHandoff, SessionHandoffError, submitSessionHandoff, waitForSessionHandoff, } from '../services/session-handoff-service.js';
10
11
  import { AgentStopError, assertAgentStopped } from '../utils/agent-stop-result.js';
11
12
  import { withGitRepoLock } from '../utils/git-repo-lock.js';
12
13
  import { assertWorkspaceLifecycleAvailable, isWorkspaceLifecycleBusy, workspaceLifecycleReason, } from '../utils/workspace-lifecycle-guard.js';
@@ -18,9 +19,11 @@ import { Hono } from 'hono';
18
19
  import { bodyLimit } from 'hono/body-limit';
19
20
  import { MAX_ATTACHMENT_REQUEST_BYTES } from '../../shared/attachments.js';
20
21
  import { AUTO_LOOP_HARD_RULES, buildAutoLoopGroomingSteps, buildGroomingIntro } from '../../shared/auto-loop-prompts.js';
22
+ import { parseQuestionSelections } from '../../shared/question-selections.js';
21
23
  import { getDb } from '../db/index.js';
22
24
  import { migrationGuard } from '../middleware/migration-guard.js';
23
25
  import { workspaceCreationMaintenance } from '../middleware/workspace-creation-maintenance.js';
26
+ import { assertEngineConfigurationSupported } from '../services/agent/engines/configuration.js';
24
27
  import { listEngines } from '../services/agent/engines/registry.js';
25
28
  import * as agentManager from '../services/agent/orchestrator.js';
26
29
  import * as archiveScriptService from '../services/archive-script-service.js';
@@ -316,7 +319,7 @@ function resolveCreateAgentPermissionMode(bodyValue, _projectPath, globalSetting
316
319
  const global = globalSettings.defaultPermissionModeByEngine?.[engineId];
317
320
  if (isAgentPermissionMode(global))
318
321
  return global;
319
- return 'bypass';
322
+ return engineId === 'mistral-vibe' ? 'interactive' : 'bypass';
320
323
  }
321
324
  /**
322
325
  * Extract the numeric PR/MR number from a forge URL.
@@ -357,7 +360,9 @@ function handoffError(c, error) {
357
360
  }
358
361
  app.post('/:id/session-handoffs', migrationGuard, async (c) => {
359
362
  try {
360
- return c.json({ handoff: createSessionHandoff(c.req.param('id'), await c.req.json()) }, 202);
363
+ const input = await c.req.json();
364
+ await preflightSessionHandoff(c.req.param('id'), input);
365
+ return c.json({ handoff: createSessionHandoff(c.req.param('id'), input) }, 202);
361
366
  }
362
367
  catch (error) {
363
368
  return handoffError(c, error);
@@ -418,7 +423,8 @@ app.post('/:id/switch-engine', migrationGuard, async (c) => {
418
423
  const engine = listEngines().find((item) => item.id === body.engine);
419
424
  if (!engine)
420
425
  return c.json({ error: `Unknown engine '${body.engine}'` }, 400);
421
- const model = typeof body.model === 'string' && engine.capabilities.models.some((item) => item.id === body.model)
426
+ const model = typeof body.model === 'string' &&
427
+ (engine.id === 'mistral-vibe' || engine.capabilities.models.some((item) => item.id === body.model))
422
428
  ? body.model
423
429
  : engine.capabilities.models[0]?.id;
424
430
  if (!model)
@@ -428,6 +434,21 @@ app.post('/:id/switch-engine', migrationGuard, async (c) => {
428
434
  return c.json({ error: `Engine '${body.engine}' does not support the selected permission mode` }, 400);
429
435
  }
430
436
  const effort = typeof body.reasoningEffort === 'string' ? body.reasoningEffort : 'auto';
437
+ try {
438
+ assertEngineConfigurationSupported(engine, {
439
+ model: body.model,
440
+ reasoningEffort: body.reasoningEffort,
441
+ agentPermissionMode: body.agentPermissionMode,
442
+ });
443
+ await preflightMistralConfiguration(engine.id, { workspaceId: workspace.id }, {
444
+ model: body.model,
445
+ reasoningEffort: body.reasoningEffort,
446
+ agentPermissionMode: body.agentPermissionMode,
447
+ });
448
+ }
449
+ catch (error) {
450
+ return c.json({ error: error instanceof Error ? error.message : 'Invalid engine configuration' }, 400);
451
+ }
431
452
  const handoff = typeof body.handoff === 'string' && body.handoff.trim()
432
453
  ? body.handoff.trim().slice(0, 30_000)
433
454
  : await buildEngineHandoff(workspace, workspace.engine, body.engine);
@@ -544,6 +565,48 @@ app.post('/', workspaceCreationMaintenance, migrationGuard, creationBodyLimit, a
544
565
  }
545
566
  }
546
567
  }
568
+ if (body.engine === 'mistral-vibe' &&
569
+ body.agentPermissionMode !== undefined &&
570
+ !isAgentPermissionMode(body.agentPermissionMode))
571
+ return c.json({ error: 'Invalid agentPermissionMode', step: 'validate' }, 400);
572
+ const selectedEngine = listEngines().find((engine) => engine.id === (body.engine ?? 'claude-code'));
573
+ if (selectedEngine) {
574
+ try {
575
+ const mode = resolveCreateAgentPermissionMode(body.agentPermissionMode, body.projectPath, settingsService.getGlobalSettings(), selectedEngine.id);
576
+ assertEngineConfigurationSupported(selectedEngine, {
577
+ model: body.model,
578
+ reasoningEffort: body.reasoningEffort,
579
+ agentPermissionMode: mode,
580
+ });
581
+ await preflightMistralConfiguration(selectedEngine.id, { projectPath: body.worktreePath ?? body.projectPath }, {
582
+ model: body.model,
583
+ reasoningEffort: body.reasoningEffort,
584
+ agentPermissionMode: mode,
585
+ });
586
+ assertEngineConfigurationSupported(selectedEngine, {
587
+ model: body.brainstormModel,
588
+ reasoningEffort: body.brainstormReasoningEffort,
589
+ });
590
+ await preflightMistralConfiguration(selectedEngine.id, { projectPath: body.worktreePath ?? body.projectPath }, {
591
+ model: body.brainstormModel ?? body.model,
592
+ reasoningEffort: body.brainstormReasoningEffort ?? body.reasoningEffort,
593
+ agentPermissionMode: mode,
594
+ });
595
+ if (body.autoLoopFinalReview) {
596
+ const review = finalReviewService.parseFinalReviewConfiguration(body.autoLoopFinalReview);
597
+ await preflightMistralConfiguration(review.engine, { projectPath: body.worktreePath ?? body.projectPath }, {
598
+ ...review,
599
+ agentPermissionMode: 'interactive',
600
+ });
601
+ }
602
+ body.agentPermissionMode = mode;
603
+ }
604
+ catch (error) {
605
+ const message = error instanceof Error ? error.message : 'Invalid engine configuration';
606
+ emitCreateFailed(creationId, 'validate', message);
607
+ return c.json({ error: message, step: 'validate' }, 400);
608
+ }
609
+ }
547
610
  // Fetch the source branch from origin first. On failure this is non-fatal:
548
611
  // fall back to the local source branch if it exists (offline / no remote),
549
612
  // and only hard-block (422) when neither origin nor a local branch is usable.
@@ -1745,7 +1808,10 @@ app.get('/:id/auto-loop/final-review', (c) => {
1745
1808
  app.patch('/:id/auto-loop/final-review', migrationGuard, async (c) => {
1746
1809
  try {
1747
1810
  const body = await c.req.json();
1748
- return c.json(finalReviewService.configureFinalReview(c.req.param('id'), body?.configuration));
1811
+ const config = finalReviewService.parseFinalReviewConfiguration(body?.configuration);
1812
+ if (config)
1813
+ await preflightMistralConfiguration(config.engine, { workspaceId: c.req.param('id') }, { ...config, agentPermissionMode: 'interactive' });
1814
+ return c.json(finalReviewService.configureFinalReview(c.req.param('id'), config));
1749
1815
  }
1750
1816
  catch (error) {
1751
1817
  return c.json({ error: error instanceof Error ? error.message : 'Invalid request' }, error instanceof ReviewRequestError ? error.status : 400);
@@ -2080,6 +2146,7 @@ app.post('/:id/deferred-tool-use/answer', async (c) => {
2080
2146
  await agentManager.answerPendingQuestion(id, body.answers, body.toolCallId, {
2081
2147
  awaitingFreeForm: body.awaitingFreeForm === true,
2082
2148
  ...(typeof body.response === 'string' ? { response: body.response } : {}),
2149
+ ...(body.selections !== undefined ? { selections: parseQuestionSelections(body.selections) } : {}),
2083
2150
  });
2084
2151
  return c.json({ ok: true });
2085
2152
  }
@@ -2871,6 +2938,25 @@ app.patch('/:id', migrationGuard, async (c) => {
2871
2938
  }, 400);
2872
2939
  }
2873
2940
  }
2941
+ const selectedEngine = listEngines().find((engine) => engine.id === workspace.engine);
2942
+ if (selectedEngine &&
2943
+ (body.model !== undefined || body.reasoningEffort !== undefined || body.agentPermissionMode !== undefined)) {
2944
+ try {
2945
+ assertEngineConfigurationSupported(selectedEngine, {
2946
+ model: body.model ?? workspace.model,
2947
+ reasoningEffort: body.reasoningEffort ?? workspace.reasoningEffort,
2948
+ agentPermissionMode: body.agentPermissionMode ?? workspace.agentPermissionMode,
2949
+ });
2950
+ await preflightMistralConfiguration(selectedEngine.id, { workspaceId: workspace.id }, {
2951
+ model: body.model ?? workspace.model,
2952
+ reasoningEffort: body.reasoningEffort ?? workspace.reasoningEffort,
2953
+ agentPermissionMode: body.agentPermissionMode ?? workspace.agentPermissionMode,
2954
+ });
2955
+ }
2956
+ catch (error) {
2957
+ return c.json({ error: error instanceof Error ? error.message : 'Invalid engine configuration' }, 400);
2958
+ }
2959
+ }
2874
2960
  if (body.status !== undefined) {
2875
2961
  if (!isWorkspaceStatus(body.status)) {
2876
2962
  return c.json({ error: `Invalid status. Must be one of: ${VALID_WORKSPACE_STATUSES.join(', ')}` }, 400);
@@ -3415,6 +3501,24 @@ app.post('/:id/start', migrationGuard, async (c) => {
3415
3501
  error: 'Workspace is compacting its context; wait until compaction finishes before sending a message',
3416
3502
  }, 409);
3417
3503
  }
3504
+ const selectedEngine = listEngines().find((engine) => engine.id === workspaceEngine);
3505
+ if (selectedEngine) {
3506
+ try {
3507
+ assertEngineConfigurationSupported(selectedEngine, {
3508
+ model: workspace.model,
3509
+ reasoningEffort: workspace.reasoningEffort,
3510
+ agentPermissionMode: workspace.agentPermissionMode,
3511
+ });
3512
+ await preflightMistralConfiguration(selectedEngine.id, { workspaceId: workspace.id }, {
3513
+ model: workspace.model,
3514
+ reasoningEffort: workspace.reasoningEffort,
3515
+ agentPermissionMode: workspace.agentPermissionMode,
3516
+ }, resume);
3517
+ }
3518
+ catch (error) {
3519
+ return c.json({ error: error instanceof Error ? error.message : 'Invalid engine configuration' }, 400);
3520
+ }
3521
+ }
3418
3522
  // A manual start supersedes any scheduled retry (quota or transient
3419
3523
  // watchdog recovery). Leaving its timer armed could spawn a second agent
3420
3524
  // after the user has already resumed the workspace.
@@ -4258,7 +4362,8 @@ app.post('/:id/merge-pr', async (c) => {
4258
4362
  code: `forge_${availability.reason ?? 'unavailable'}`,
4259
4363
  }, 409);
4260
4364
  }
4261
- const snapshot = await provider.getPrStatus(workspace.worktreePath, workspace.workingBranch);
4365
+ // Record OPEN before merging, even if the watcher has not run since startup.
4366
+ const snapshot = await refreshPrSnapshot(id);
4262
4367
  if (snapshot?.state !== 'OPEN') {
4263
4368
  return c.json({ error: 'No open PR found for this workspace', code: 'pr_not_open' }, 409);
4264
4369
  }
@@ -4302,13 +4407,15 @@ app.post('/:id/delete-remote-branch', async (c) => {
4302
4407
  if (!provider.capabilities.canDeleteRemoteBranch) {
4303
4408
  return c.json({ error: 'This project cannot delete remote branches', code: 'forge_unsupported' }, 409);
4304
4409
  }
4305
- const availability = await provider.isAvailable(workspace.worktreePath);
4410
+ // The merge watcher may have already archived and purged this worktree.
4411
+ const repoPath = workspace.projectPath;
4412
+ const availability = await provider.isAvailable(repoPath);
4306
4413
  if (!availability.available)
4307
4414
  return c.json({ error: 'Forge CLI unavailable', code: 'forge_unavailable' }, 409);
4308
- const snapshot = await provider.getPrStatus(workspace.worktreePath, workspace.workingBranch);
4415
+ const snapshot = await provider.getPrStatus(repoPath, workspace.workingBranch);
4309
4416
  if (snapshot?.state !== 'MERGED')
4310
4417
  return c.json({ error: 'The PR is not merged yet', code: 'pr_not_merged' }, 409);
4311
- await provider.deleteRemoteBranch(workspace.worktreePath, workspace.workingBranch);
4418
+ await provider.deleteRemoteBranch(repoPath, workspace.workingBranch);
4312
4419
  return c.json({ success: true });
4313
4420
  }
4314
4421
  catch (err) {
@@ -18,6 +18,8 @@ export const CLAUDE_CODE_CAPABILITIES = {
18
18
  permissionModes: ['plan', 'bypass', 'strict', 'interactive'],
19
19
  supportsResume: true,
20
20
  supportsMcp: true,
21
+ supportsLiveSteering: true,
22
+ supportsReadOnly: true,
21
23
  supportsSkills: true,
22
24
  supportsSubagents: true,
23
25
  supportsQuotaStatus: true,
@@ -1049,6 +1049,9 @@ export function createClaudeCodeEngine() {
1049
1049
  // swallow — best effort
1050
1050
  }
1051
1051
  },
1052
+ hasPendingUserInput(toolCallId) {
1053
+ return pendingResolvers.has(toolCallId);
1054
+ },
1052
1055
  resolvePendingUserInput(toolCallId, response) {
1053
1056
  const resolver = pendingResolvers.get(toolCallId);
1054
1057
  if (!resolver)
@@ -13,6 +13,8 @@ export const CODEX_CAPABILITIES = {
13
13
  permissionModes: ['plan', 'bypass', 'strict', 'interactive'],
14
14
  supportsResume: true,
15
15
  supportsMcp: true,
16
+ supportsLiveSteering: true,
17
+ supportsReadOnly: true,
16
18
  supportsSkills: false,
17
19
  supportsSubagents: true,
18
20
  supportsQuotaStatus: true,
@@ -692,6 +692,9 @@ export function createCodexEngine() {
692
692
  }
693
693
  await terminateChild();
694
694
  },
695
+ hasPendingUserInput(callId) {
696
+ return pendingByCallId.has(callId);
697
+ },
695
698
  resolvePendingUserInput(callId, response) {
696
699
  const pending = pendingByCallId.get(callId);
697
700
  if (!pending)
@@ -0,0 +1,14 @@
1
+ import { assertMistralSelection } from './mistral-vibe/configuration.js';
2
+ /** Validate native selections before creating or replacing launch state. */
3
+ export function assertEngineConfigurationSupported(engine, selection) {
4
+ if (selection.readOnly && !engine.capabilities.supportsReadOnly)
5
+ throw new Error(`Engine '${engine.id}' does not support read-only launches`);
6
+ // Native membership is checked contextually and again on the actual ACP session.
7
+ if (engine.id !== 'mistral-vibe')
8
+ return;
9
+ const mode = selection.agentPermissionMode ?? 'interactive';
10
+ if (!engine.capabilities.permissionModes.some((item) => item === mode))
11
+ throw new Error(`Engine '${engine.id}' does not support agentPermissionMode '${String(mode)}'`);
12
+ assertMistralSelection(selection.model, 'model');
13
+ assertMistralSelection(selection.reasoningEffort, 'thinking');
14
+ }
@@ -0,0 +1,13 @@
1
+ import { MISTRAL_MODELS } from '../../../../../shared/mistral-models.js';
2
+ export const MISTRAL_CAPABILITIES = {
3
+ models: MISTRAL_MODELS.map((model) => ({ id: model.id, label: model.label })),
4
+ effortLevels: [{ id: 'auto', label: 'Auto' }],
5
+ permissionModes: ['interactive', 'strict', 'bypass'],
6
+ supportsResume: true,
7
+ supportsMcp: true,
8
+ supportsLiveSteering: false,
9
+ supportsReadOnly: false,
10
+ supportsSkills: false,
11
+ supportsSubagents: false,
12
+ supportsQuotaStatus: false,
13
+ };
@@ -0,0 +1,64 @@
1
+ import { Readable, Writable } from 'node:stream';
2
+ import * as acp from '@agentclientprotocol/sdk';
3
+ /** The SDK owns framing and incoming validation. Stderr never enters ACP. */
4
+ export function connectVibeAcp(child, handlers) {
5
+ if (!child.stdin || !child.stdout)
6
+ throw new Error('Vibe ACP requires piped stdin and stdout');
7
+ const app = acp.client({ name: 'kobo' });
8
+ if (handlers.sessionUpdate)
9
+ app.onNotification(acp.methods.client.session.update, handlers.sessionUpdate);
10
+ if (handlers.requestPermission)
11
+ app.onRequest(acp.methods.client.session.requestPermission, handlers.requestPermission);
12
+ if (handlers.createElicitation)
13
+ app.onRequest(acp.methods.client.elicitation.create, handlers.createElicitation);
14
+ // Node and DOM disagree on BYOB generics; both adapters are standard Web Streams at runtime.
15
+ const readable = Readable.toWeb(child.stdout);
16
+ return app.connect(acp.ndJsonStream(Writable.toWeb(child.stdin), readable));
17
+ }
18
+ /** Fluent SDK responses are unchecked; validate the fields this transport consumes. */
19
+ function validateResponse(method, response) {
20
+ const value = response;
21
+ if (method === acp.methods.agent.initialize && value?.protocolVersion !== acp.PROTOCOL_VERSION) {
22
+ throw new Error('Invalid Vibe ACP protocol version; ACP v1 is required');
23
+ }
24
+ if (method === acp.methods.agent.session.new &&
25
+ (typeof value?.sessionId !== 'string' || value.sessionId.trim().length === 0)) {
26
+ throw new Error('Invalid Vibe ACP session identity');
27
+ }
28
+ if (method === acp.methods.agent.session.prompt &&
29
+ (typeof value?.stopReason !== 'string' ||
30
+ !['end_turn', 'max_tokens', 'max_turn_requests', 'refusal', 'cancelled'].includes(value.stopReason))) {
31
+ throw new Error('Invalid Vibe ACP prompt stop reason');
32
+ }
33
+ }
34
+ /** Use for all requests: initialization/setup are bounded, prompts have no absolute deadline. */
35
+ export async function requestVibeAcp(connection, method, params) {
36
+ const timeout = method === acp.methods.agent.initialize ? 20000 : 30000;
37
+ let timer;
38
+ const pending = connection.agent.request(method, params);
39
+ try {
40
+ const response = await (method === acp.methods.agent.session.prompt
41
+ ? pending
42
+ : Promise.race([
43
+ pending,
44
+ new Promise((_resolve, reject) => {
45
+ timer = setTimeout(() => {
46
+ const error = new Error(`Vibe ACP ${method} timed out after ${timeout}ms`);
47
+ reject(error);
48
+ }, timeout);
49
+ }),
50
+ ]));
51
+ validateResponse(method, response);
52
+ return response;
53
+ }
54
+ finally {
55
+ clearTimeout(timer);
56
+ }
57
+ }
58
+ /** Pass as runtime.stop's callback; the runtime still ends stdin and confirms process exit. */
59
+ export async function closeVibeAcpSession(connection, session) {
60
+ if (session.active)
61
+ await connection.agent.notify(acp.methods.agent.session.cancel, { sessionId: session.sessionId });
62
+ if (session.canClose)
63
+ await requestVibeAcp(connection, acp.methods.agent.session.close, { sessionId: session.sessionId });
64
+ }
@@ -0,0 +1,76 @@
1
+ export const MISTRAL_MODES = { interactive: 'ask', strict: 'accept-edits', bypass: 'auto-approve' };
2
+ export function assertMistralSelection(value, label) {
3
+ if (value !== undefined && !text(value))
4
+ throw new Error(`Invalid Mistral Vibe ${label}; expected a nonempty native value of at most 256 characters`);
5
+ }
6
+ function record(value) {
7
+ return !!value && typeof value === 'object' && !Array.isArray(value);
8
+ }
9
+ function text(value, max = 256) {
10
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: Native identities and labels must exclude control characters.
11
+ return typeof value === 'string' && !!value.trim() && value.length <= max && !/[\x00-\x1f\x7f]/.test(value);
12
+ }
13
+ function invalid() {
14
+ throw new Error('Invalid Vibe ACP configuration options');
15
+ }
16
+ /** SDK responses are unchecked. Keep only bounded, validated native display fields. */
17
+ export function readMistralSelect(response, id, required = true) {
18
+ if (!record(response) || !Array.isArray(response.configOptions) || response.configOptions.length > 64)
19
+ return invalid();
20
+ const ids = new Set();
21
+ for (const item of response.configOptions) {
22
+ if (!record(item) || !text(item.id) || ids.has(item.id))
23
+ return invalid();
24
+ ids.add(item.id);
25
+ }
26
+ const selected = response.configOptions.find((item) => item.id === id);
27
+ if (!selected && !required)
28
+ return undefined;
29
+ if (selected?.type !== 'select' ||
30
+ !text(selected.currentValue) ||
31
+ !Array.isArray(selected.options) ||
32
+ selected.options.length > 256)
33
+ return invalid();
34
+ const options = [];
35
+ const values = new Set();
36
+ const groups = new Set();
37
+ const append = (item, group) => {
38
+ if (!record(item) ||
39
+ !text(item.value) ||
40
+ !text(item.name) ||
41
+ values.has(item.value) ||
42
+ (item.description != null && !text(item.description, 2048)) ||
43
+ options.length >= 256)
44
+ return invalid();
45
+ values.add(item.value);
46
+ options.push({
47
+ value: item.value,
48
+ name: item.name,
49
+ ...(typeof item.description === 'string' ? { description: item.description } : {}),
50
+ ...(group ? { group } : {}),
51
+ });
52
+ };
53
+ for (const item of selected.options) {
54
+ if (record(item) && 'options' in item) {
55
+ if (!text(item.group) ||
56
+ !text(item.name) ||
57
+ groups.has(item.group) ||
58
+ !Array.isArray(item.options) ||
59
+ item.options.length > 256)
60
+ return invalid();
61
+ groups.add(item.group);
62
+ for (const option of item.options)
63
+ append(option, item.name);
64
+ }
65
+ else
66
+ append(item);
67
+ }
68
+ if (!values.has(selected.currentValue))
69
+ return invalid();
70
+ return { currentValue: selected.currentValue, options };
71
+ }
72
+ export function requireMistralOption(response, id, value, confirmed = false) {
73
+ const option = readMistralSelect(response, id);
74
+ if (!option.options.some((item) => item.value === value) || (confirmed && option.currentValue !== value))
75
+ throw new Error(`Mistral Vibe ${id} selection is unavailable or overridden by the native profile; refresh native options`);
76
+ }