@loicngr/kobo 2.1.13 → 2.1.16
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.
- package/AGENTS.md +38 -6
- package/CHANGELOG.md +12 -0
- package/CONFIGURATION.md +41 -5
- package/README.md +4 -2
- package/dist/mcp-server/kobo-tasks-server.js +62 -105
- package/dist/mcp-server/workspace-dialogue-client.js +15 -3
- package/dist/server/db/auto-loop-final-review-schema.js +18 -0
- package/dist/server/db/migrations.js +12 -1
- package/dist/server/db/review-return-schema.js +18 -1
- package/dist/server/db/schema.js +4 -0
- package/dist/server/db/workspace-message-batch-schema.js +9 -0
- package/dist/server/index.js +8 -1
- package/dist/server/routes/mcp.js +69 -3
- package/dist/server/routes/workspace-messages.js +22 -0
- package/dist/server/routes/workspaces.js +119 -4
- package/dist/server/services/agent/engines/claude-code/engine.js +37 -1
- package/dist/server/services/agent/orchestrator.js +159 -40
- package/dist/server/services/agent/session-controller.js +4 -1
- package/dist/server/services/auto-loop-final-review-service.js +329 -0
- package/dist/server/services/auto-loop-service.js +53 -10
- package/dist/server/services/pr-checkout-rollback-service.js +51 -0
- package/dist/server/services/pr-checkout-service.js +21 -1
- package/dist/server/services/review-launch-runtime.js +17 -0
- package/dist/server/services/review-return-service.js +58 -10
- package/dist/server/services/review-service.js +59 -11
- package/dist/server/services/workspace-creation-mcp-service.js +198 -0
- package/dist/server/services/workspace-group-message-mcp-service.js +58 -0
- package/dist/server/services/workspace-group-message-service.js +182 -0
- package/dist/server/services/workspace-lifecycle-mcp-service.js +72 -0
- package/dist/server/services/workspace-message-service.js +5 -0
- package/dist/server/services/workspace-service.js +11 -0
- package/dist/server/services/workspace-template-service.js +10 -0
- package/dist/shared/auto-loop-review.js +1 -0
- package/dist/shared/pr-checkout-steps.js +86 -0
- package/dist/shared/review-return.js +1 -0
- package/dist/shared/workspace-creation-tools.js +182 -0
- package/dist/shared/workspace-group-message-tools.js +134 -0
- package/dist/shared/workspace-group-messages.js +27 -0
- package/dist/shared/workspace-lifecycle-tools.js +112 -0
- package/package.json +1 -1
- package/src/client/dist/pwa/assets/ActivityFeed-tJmJhmtl.js +2 -0
- package/src/client/dist/pwa/assets/AutoLoopFinalReviewSettings-Bbrq02Xt.js +1 -0
- package/src/client/dist/pwa/assets/AutoLoopFinalReviewSettings-Q-OlSQ1c.css +1 -0
- package/src/client/dist/pwa/assets/{ChangelogPage-DD90Nl6c.js → ChangelogPage-BJfkAVYp.js} +1 -1
- package/src/client/dist/pwa/assets/{ComparisonPanel-D_BQFWiT.js → ComparisonPanel-BfP8APKY.js} +1 -1
- package/src/client/dist/pwa/assets/CreatePage-BbtNfo0x.js +2 -0
- package/src/client/dist/pwa/assets/CreatePage-DxOes_cF.css +1 -0
- package/src/client/dist/pwa/assets/{DashboardPage-DyvTdmBg.js → DashboardPage-DZYsAh5y.js} +1 -1
- package/src/client/dist/pwa/assets/DiffViewer-4Q-GJgyA.js +8 -0
- package/src/client/dist/pwa/assets/{DocumentsPanel-bm639sIu.js → DocumentsPanel-BfPlfiwU.js} +1 -1
- package/src/client/dist/pwa/assets/{DrawerToggleButton-KfwSzjeY.js → DrawerToggleButton-DG1Qjqk1.js} +1 -1
- package/src/client/dist/pwa/assets/GitPanel-ECnbb_rx.js +3 -0
- package/src/client/dist/pwa/assets/{HealthPage-37IHOIW-.js → HealthPage-_VXG1Zeo.js} +1 -1
- package/src/client/dist/pwa/assets/MainLayout-BH6grTuW.js +2 -0
- package/src/client/dist/pwa/assets/MainLayout-DnmUxNk7.css +1 -0
- package/src/client/dist/pwa/assets/{MemoryPanel-CucZMhjy.js → MemoryPanel-CVlvGZXZ.js} +1 -1
- package/src/client/dist/pwa/assets/{MemoryProposalList-CUQybzVR.js → MemoryProposalList-SJsnzUAM.js} +1 -1
- package/src/client/dist/pwa/assets/{QBtnDropdown-x6qPyl_7.js → QBtnDropdown-DBgGsN-D.js} +1 -1
- package/src/client/dist/pwa/assets/{QExpansionItem-D33aoRoG.js → QExpansionItem-DKgaMJ_l.js} +1 -1
- package/src/client/dist/pwa/assets/{QScrollArea-Di-D6t2u.js → QScrollArea-D3OCsq3g.js} +1 -1
- package/src/client/dist/pwa/assets/QSelect-Cb_s_nox.js +1 -0
- package/src/client/dist/pwa/assets/{QTooltip-BhwG7lp2.js → QTooltip-X5ps8Opw.js} +1 -1
- package/src/client/dist/pwa/assets/{SchedulePanel-qFi9Wxzh.js → SchedulePanel-Ww_FVlW5.js} +1 -1
- package/src/client/dist/pwa/assets/{SearchPage-DA9nRmsA.js → SearchPage-B87WEHf3.js} +1 -1
- package/src/client/dist/pwa/assets/SettingsPage-BAZLGUyg.js +9 -0
- package/src/client/dist/pwa/assets/{SplitWorkspacePage-Brw2XsYJ.js → SplitWorkspacePage-B99AJUbG.js} +1 -1
- package/src/client/dist/pwa/assets/{TerminalPanel-DVP18-nk.js → TerminalPanel-DnA1MXwu.js} +9 -9
- package/src/client/dist/pwa/assets/{ToolCallItem-cSHh3p_F.js → ToolCallItem-2vFggaur.js} +3 -3
- package/src/client/dist/pwa/assets/{TouchPan-C6LZZ3rw.js → TouchPan-BSU3z0aO.js} +1 -1
- package/src/client/dist/pwa/assets/{TourReplayButton-Czlqo18H.js → TourReplayButton-DQwS2xdD.js} +1 -1
- package/src/client/dist/pwa/assets/{WorkspaceAttentionLabels-BDYGq0pC.js → WorkspaceAttentionLabels-CM1tGDje.js} +1 -1
- package/src/client/dist/pwa/assets/WorkspacePage-B248nKIU.css +1 -0
- package/src/client/dist/pwa/assets/WorkspacePage-CQN6EKIn.js +4 -0
- package/src/client/dist/pwa/assets/{browser-ifCwUGpE.js → browser-DPsupI9Q.js} +1 -1
- package/src/client/dist/pwa/assets/{build-path-tree-BBkGq3lN.js → build-path-tree-CbIdt-p9.js} +1 -1
- package/src/client/dist/pwa/assets/custom-sounds-DR68eyF8.js +1 -0
- package/src/client/dist/pwa/assets/de-D3Ycvf_F.js +17 -0
- package/src/client/dist/pwa/assets/{editor.api-CV_seGJR.js → editor.api-DvgAtnOj.js} +1 -1
- package/src/client/dist/pwa/assets/{efforts-ttw0bL63.js → efforts-nmxfTVQD.js} +1 -1
- package/src/client/dist/pwa/assets/es-TgeS5898.js +17 -0
- package/src/client/dist/pwa/assets/{expand-template-C8biMRXo.js → expand-template-Brn5O4a1.js} +1 -1
- package/src/client/dist/pwa/assets/{format-Cu95dbxt.js → format-DfJ3Fv1i.js} +1 -1
- package/src/client/dist/pwa/assets/{formatters-C25D1RSd.js → formatters-B30bNFhq.js} +1 -1
- package/src/client/dist/pwa/assets/fr-CJafryNb.js +17 -0
- package/src/client/dist/pwa/assets/{freemarker2-DJEFr74q.js → freemarker2-DnZe74tc.js} +1 -1
- package/src/client/dist/pwa/assets/{handlebars-C_lzsMjX.js → handlebars-CmuMfxzO.js} +1 -1
- package/src/client/dist/pwa/assets/{html-CWmLFAmY.js → html-BD7OJFGX.js} +1 -1
- package/src/client/dist/pwa/assets/i18n-BY4JeBjk.js +1 -0
- package/src/client/dist/pwa/assets/index-C5fAw9ll.js +20 -0
- package/src/client/dist/pwa/assets/it-C5mN8Y61.js +17 -0
- package/src/client/dist/pwa/assets/{javascript-tsuiEUe-.js → javascript-Y-OrgcmW.js} +1 -1
- package/src/client/dist/pwa/assets/{liquid-BBr9BvUk.js → liquid-BqOkKePH.js} +1 -1
- package/src/client/dist/pwa/assets/{mdx-gjBH1cXG.js → mdx-D-58x2h0.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco-contributions-CvGcBIsb.js → monaco-contributions-BW4ZMKO6.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco.contribution-Uw-Lfb8Z.js → monaco.contribution-DJWqzNG-.js} +2 -2
- package/src/client/dist/pwa/assets/network-auth-BGPRhUa6.js +1 -0
- package/src/client/dist/pwa/assets/{notify-theme-PMSxqP6t.js → notify-theme-DZDzfxph.js} +1 -1
- package/src/client/dist/pwa/assets/{project-color-BDQa2TgU.js → project-color-gB5yeZPN.js} +1 -1
- package/src/client/dist/pwa/assets/{python-DlIgD1n6.js → python-BkBj1wDC.js} +1 -1
- package/src/client/dist/pwa/assets/{razor-AraFPlpM.js → razor-DOOJ_0b8.js} +1 -1
- package/src/client/dist/pwa/assets/{typescript-CB8UpXHM.js → typescript-Bheo43G5.js} +1 -1
- package/src/client/dist/pwa/assets/{use-search-index-status-CSREHQM4.js → use-search-index-status-CBpnyV-R.js} +1 -1
- package/src/client/dist/pwa/assets/use-tours-BHNasCvg.js +2 -0
- package/src/client/dist/pwa/assets/{use-worktree-restore-BM3Giig1.js → use-worktree-restore-CgABsWno.js} +1 -1
- package/src/client/dist/pwa/assets/{workspace-templates-BPMFvTnQ.js → workspace-templates-BnFkigYg.js} +1 -1
- package/src/client/dist/pwa/assets/{xml-uRjL1P43.js → xml-vHt2BD26.js} +1 -1
- package/src/client/dist/pwa/assets/{yaml-C9CFR4qV.js → yaml-BvhLPzP9.js} +1 -1
- package/src/client/dist/pwa/index.html +1 -1
- package/src/client/dist/pwa/sw.js +1 -1
- package/src/mcp-server/README.md +156 -12
- package/src/mcp-server/kobo-tasks-server.ts +73 -106
- package/src/mcp-server/workspace-dialogue-client.ts +18 -3
- package/src/client/dist/pwa/assets/ActivityFeed-BTRzc60o.js +0 -2
- package/src/client/dist/pwa/assets/CreatePage-BZAoCukb.js +0 -2
- package/src/client/dist/pwa/assets/CreatePage-CdCs8PRS.css +0 -1
- package/src/client/dist/pwa/assets/DiffViewer-D0K2PlZD.js +0 -8
- package/src/client/dist/pwa/assets/GitPanel-C-Ejlc9_.js +0 -3
- package/src/client/dist/pwa/assets/MainLayout-BQOOup9o.css +0 -1
- package/src/client/dist/pwa/assets/MainLayout-DXQ1ouYl.js +0 -2
- package/src/client/dist/pwa/assets/QSelect-MdPXvwm4.js +0 -1
- package/src/client/dist/pwa/assets/SettingsPage-DcV6apqE.js +0 -9
- package/src/client/dist/pwa/assets/WorkspacePage-BwUkijpS.js +0 -4
- package/src/client/dist/pwa/assets/WorkspacePage-Bwfv559o.css +0 -1
- package/src/client/dist/pwa/assets/custom-sounds-CeSgjIY7.js +0 -1
- package/src/client/dist/pwa/assets/de-CHY2BAfL.js +0 -17
- package/src/client/dist/pwa/assets/es-DbcSKp38.js +0 -17
- package/src/client/dist/pwa/assets/fr-CyyJ8S2c.js +0 -17
- package/src/client/dist/pwa/assets/i18n-CobVQiTM.js +0 -1
- package/src/client/dist/pwa/assets/index-ImUH7Bbt.js +0 -20
- package/src/client/dist/pwa/assets/it-XQJW2Xkr.js +0 -17
- package/src/client/dist/pwa/assets/network-auth-D0Vm6a09.js +0 -1
- package/src/client/dist/pwa/assets/use-tours-6yxNVmjH.js +0 -2
package/dist/server/index.js
CHANGED
|
@@ -32,9 +32,10 @@ import soundsRouter from './routes/sounds.js';
|
|
|
32
32
|
import templatesRouter from './routes/templates.js';
|
|
33
33
|
import usageRoutes from './routes/usage.js';
|
|
34
34
|
import voiceRouter from './routes/voice.js';
|
|
35
|
+
import workspaceMessagesRouter from './routes/workspace-messages.js';
|
|
35
36
|
import workspaceTemplatesRouter from './routes/workspace-templates.js';
|
|
36
37
|
import workspacesRouter from './routes/workspaces.js';
|
|
37
|
-
import { getAvailableSkills, reconcileOrphanSessions, restoreRetryCountsFromDb, setBackendPort, startAgent, startWatchdog, stopAgentAndWait, stopAllAgents, stopWatchdog, } from './services/agent/orchestrator.js';
|
|
38
|
+
import { getAvailableSkills, reconcileOrphanSessions, restoreRetryCountsFromDb, resumePendingReviewReturns, setBackendPort, startAgent, startWatchdog, stopAgentAndWait, stopAllAgents, stopWatchdog, } from './services/agent/orchestrator.js';
|
|
38
39
|
import * as autoLoopService from './services/auto-loop-service.js';
|
|
39
40
|
import { startAwaitingUserReminder, stopAwaitingUserReminder } from './services/awaiting-user-reminder-service.js';
|
|
40
41
|
import { runContentMigrationIfNeeded } from './services/content-migration-service.js';
|
|
@@ -57,6 +58,7 @@ import { startUpdateChecker, stopUpdateChecker } from './services/update-check-s
|
|
|
57
58
|
import { startUsagePoller, stopUsagePoller } from './services/usage/index.js';
|
|
58
59
|
import * as wakeupService from './services/wakeup-service.js';
|
|
59
60
|
import { handleConnection, setMessageHandler } from './services/websocket-service.js';
|
|
61
|
+
import { reconcileGroupMessageBatches, stopGroupMessageBatches } from './services/workspace-group-message-service.js';
|
|
60
62
|
import { deliverWorkspaceMessage } from './services/workspace-message-service.js';
|
|
61
63
|
import { getWorkspace } from './services/workspace-service.js';
|
|
62
64
|
import { resolveRetentionConfig } from './services/ws-events-retention-service.js';
|
|
@@ -84,6 +86,7 @@ catch (err) {
|
|
|
84
86
|
}
|
|
85
87
|
runMigrations(db);
|
|
86
88
|
reconcileMessageRequests(db);
|
|
89
|
+
reconcileGroupMessageBatches(db);
|
|
87
90
|
reconcileMemoryContextsOnStartup();
|
|
88
91
|
startSearchIndex();
|
|
89
92
|
// Event retention. OPT-IN: disabled by default, so this is a no-op until the
|
|
@@ -155,6 +158,7 @@ app.use('/api/*', networkAuthMiddleware);
|
|
|
155
158
|
app.get('/api/health', (c) => c.json({ status: 'ok', version: getPackageVersion() }));
|
|
156
159
|
// Mount route sub-routers
|
|
157
160
|
app.route('/api/workspaces', workspacesRouter);
|
|
161
|
+
app.route('/api/workspace-messages', workspaceMessagesRouter);
|
|
158
162
|
app.route('/api/mcp', mcpRouter);
|
|
159
163
|
app.route('/api/memory', memoryRouter);
|
|
160
164
|
app.route('/api/pull-requests', pullRequestsRouter);
|
|
@@ -240,6 +244,7 @@ const server = serve({
|
|
|
240
244
|
// ephemeral or SERVER_PORT-selected port. Quota ownership comes first.
|
|
241
245
|
restoreRetryCountsFromDb();
|
|
242
246
|
quotaBackoffService.restoreOnBoot((workspaceId, pending) => autoLoopService.onQuotaBackoffExpired(workspaceId, pending));
|
|
247
|
+
resumePendingReviewReturns();
|
|
243
248
|
autoLoopService.rehydrate();
|
|
244
249
|
wakeupService.rehydrate();
|
|
245
250
|
cronService.restoreOnBoot();
|
|
@@ -412,6 +417,7 @@ async function gracefulShutdown(signal, exitCode = 0) {
|
|
|
412
417
|
return;
|
|
413
418
|
isShuttingDown = true;
|
|
414
419
|
stopUpdateChecker();
|
|
420
|
+
const groupMessagesStopped = stopGroupMessageBatches();
|
|
415
421
|
console.log(`\n[kobo] Received ${signal}, shutting down gracefully…`);
|
|
416
422
|
const forceExitTimer = setTimeout(() => {
|
|
417
423
|
console.error('[kobo] Graceful shutdown timed out; forcing exit');
|
|
@@ -478,6 +484,7 @@ async function gracefulShutdown(signal, exitCode = 0) {
|
|
|
478
484
|
finally {
|
|
479
485
|
try {
|
|
480
486
|
const stopped = await Promise.allSettled([
|
|
487
|
+
groupMessagesStopped,
|
|
481
488
|
stopWsEventsRetention(),
|
|
482
489
|
stopSearchIndex(),
|
|
483
490
|
dailyBackupScheduler.stop(),
|
|
@@ -4,21 +4,73 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprot
|
|
|
4
4
|
import { Hono } from 'hono';
|
|
5
5
|
import { bodyLimit } from 'hono/body-limit';
|
|
6
6
|
import { EXTERNAL_MEMORY_TOOL_DEFINITIONS, isMemoryToolName } from '../../shared/memory-tools.js';
|
|
7
|
+
import { isWorkspaceCreationTool, MAX_MCP_CREATION_REQUEST_BYTES, WORKSPACE_CREATION_TOOLS, } from '../../shared/workspace-creation-tools.js';
|
|
7
8
|
import { EXTERNAL_MCP_TOOLS } from '../../shared/workspace-dialogue-tools.js';
|
|
9
|
+
import { isWorkspaceGroupMessageTool, WORKSPACE_GROUP_MESSAGE_TOOLS, } from '../../shared/workspace-group-message-tools.js';
|
|
10
|
+
import { isWorkspaceLifecycleTool, WORKSPACE_LIFECYCLE_TOOLS } from '../../shared/workspace-lifecycle-tools.js';
|
|
8
11
|
import { createMcpClientContext } from '../services/mcp-client-context.js';
|
|
9
12
|
import { executeMemoryMcpTool } from '../services/memory-mcp-service.js';
|
|
13
|
+
import { executeWorkspaceCreationTool, WorkspaceCreationMcpError } from '../services/workspace-creation-mcp-service.js';
|
|
10
14
|
import { executeWorkspaceDialogueTool } from '../services/workspace-dialogue-service.js';
|
|
15
|
+
import { executeWorkspaceGroupMessageTool, WorkspaceGroupMessageMcpError, } from '../services/workspace-group-message-mcp-service.js';
|
|
16
|
+
import { executeWorkspaceLifecycleTool, WorkspaceLifecycleMcpError, } from '../services/workspace-lifecycle-mcp-service.js';
|
|
11
17
|
import { prepareMemoryToolEnvelope } from '../utils/memory-token-budget.js';
|
|
12
18
|
const app = new Hono();
|
|
13
|
-
|
|
19
|
+
// Only creation envelopes may carry the larger, base64 attachment payload.
|
|
20
|
+
// Other tools retain their existing 1 MiB limit, including malformed envelopes.
|
|
21
|
+
app.use('*', bodyLimit({ maxSize: MAX_MCP_CREATION_REQUEST_BYTES }));
|
|
22
|
+
app.use('*', async (c, next) => {
|
|
23
|
+
if (c.req.raw.body) {
|
|
24
|
+
const body = await c.req.raw.clone().arrayBuffer();
|
|
25
|
+
if (body.byteLength > 1024 * 1024) {
|
|
26
|
+
let creation = false;
|
|
27
|
+
try {
|
|
28
|
+
const envelope = JSON.parse(new TextDecoder().decode(body));
|
|
29
|
+
creation = envelope?.method === 'tools/call' && envelope?.params?.name === 'create_workspace';
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
/* Preserve the normal body-size rejection for malformed input. */
|
|
33
|
+
}
|
|
34
|
+
if (!creation)
|
|
35
|
+
return c.json({ error: 'MCP request is too large' }, 413);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return next();
|
|
39
|
+
});
|
|
40
|
+
/** Dispatch only the supported MCP operations through the existing UI handlers. */
|
|
41
|
+
async function dispatchWorkspaceRequest(pathname, init) {
|
|
42
|
+
if (pathname === '/api/workspaces/archived')
|
|
43
|
+
throw new Error('Bulk operations are not supported by workspace MCP tools');
|
|
44
|
+
const isWorkspaceOperation = (init.method === 'POST' &&
|
|
45
|
+
(pathname === '/api/workspaces' ||
|
|
46
|
+
/^\/api\/workspaces\/[^/]+\/(unarchive|archive|purge-worktree|restore-worktree)$/.test(pathname))) ||
|
|
47
|
+
((init.method === 'GET' || init.method === 'DELETE') && /^\/api\/workspaces\/[^/]+$/.test(pathname));
|
|
48
|
+
if (isWorkspaceOperation) {
|
|
49
|
+
const { default: routes } = await import('./workspaces.js');
|
|
50
|
+
return routes.request(`http://kobo.internal${pathname.slice('/api/workspaces'.length) || '/'}`, init);
|
|
51
|
+
}
|
|
52
|
+
if (init.method === 'POST' &&
|
|
53
|
+
(pathname === '/api/pull-requests/diagnose' || pathname === '/api/pull-requests/resolve')) {
|
|
54
|
+
const { default: routes } = await import('./pull-requests.js');
|
|
55
|
+
return routes.request(`http://kobo.internal${pathname.slice('/api/pull-requests'.length)}`, init);
|
|
56
|
+
}
|
|
57
|
+
throw new Error('Unsupported workspace operation');
|
|
58
|
+
}
|
|
14
59
|
app.all('/', async (c) => {
|
|
15
60
|
const server = new Server({ name: 'kobo-workspaces', version: '1.0.0' }, { capabilities: { tools: {} } });
|
|
16
61
|
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
17
62
|
sessionIdGenerator: undefined,
|
|
18
63
|
enableJsonResponse: true,
|
|
64
|
+
maxRequestBodySize: MAX_MCP_CREATION_REQUEST_BYTES,
|
|
19
65
|
});
|
|
20
66
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
21
|
-
tools: [
|
|
67
|
+
tools: [
|
|
68
|
+
...EXTERNAL_MCP_TOOLS,
|
|
69
|
+
...WORKSPACE_CREATION_TOOLS,
|
|
70
|
+
...WORKSPACE_LIFECYCLE_TOOLS,
|
|
71
|
+
...WORKSPACE_GROUP_MESSAGE_TOOLS,
|
|
72
|
+
...EXTERNAL_MEMORY_TOOL_DEFINITIONS,
|
|
73
|
+
],
|
|
22
74
|
}));
|
|
23
75
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
24
76
|
const isMemory = isMemoryToolName(request.params.name);
|
|
@@ -26,7 +78,13 @@ app.all('/', async (c) => {
|
|
|
26
78
|
const source = createMcpClientContext(c.req.header('X-Kobo-Client-Name'), c.req.header('X-Kobo-Client-Transport') === 'stdio' ? 'stdio' : 'http', c.req.header('X-Kobo-Client-Name-Encoding'));
|
|
27
79
|
const data = isMemory
|
|
28
80
|
? executeMemoryMcpTool(request.params.name, request.params.arguments ?? {}, source)
|
|
29
|
-
:
|
|
81
|
+
: isWorkspaceCreationTool(request.params.name)
|
|
82
|
+
? await executeWorkspaceCreationTool(request.params.name, request.params.arguments ?? {}, dispatchWorkspaceRequest)
|
|
83
|
+
: isWorkspaceLifecycleTool(request.params.name)
|
|
84
|
+
? await executeWorkspaceLifecycleTool(request.params.name, request.params.arguments ?? {}, dispatchWorkspaceRequest)
|
|
85
|
+
: isWorkspaceGroupMessageTool(request.params.name)
|
|
86
|
+
? executeWorkspaceGroupMessageTool(request.params.name, request.params.arguments ?? {}, source)
|
|
87
|
+
: await executeWorkspaceDialogueTool(request.params.name, request.params.arguments ?? {}, source);
|
|
30
88
|
if (isMemory) {
|
|
31
89
|
const bounded = prepareMemoryToolEnvelope(data, { targetTokens: 1_000, transport: 'external' });
|
|
32
90
|
if (bounded.data.memoryOutputSuppressed === true)
|
|
@@ -46,6 +104,14 @@ app.all('/', async (c) => {
|
|
|
46
104
|
};
|
|
47
105
|
}
|
|
48
106
|
catch (error) {
|
|
107
|
+
if (error instanceof WorkspaceGroupMessageMcpError) {
|
|
108
|
+
const data = { error: error.message, status: error.status };
|
|
109
|
+
return { isError: true, content: [{ type: 'text', text: JSON.stringify(data) }], structuredContent: data };
|
|
110
|
+
}
|
|
111
|
+
if (error instanceof WorkspaceCreationMcpError || error instanceof WorkspaceLifecycleMcpError) {
|
|
112
|
+
const data = { error: error.message, status: error.status, stage: error.stage, details: error.details };
|
|
113
|
+
return { isError: true, content: [{ type: 'text', text: JSON.stringify(data) }], structuredContent: data };
|
|
114
|
+
}
|
|
49
115
|
if (isMemory) {
|
|
50
116
|
const bounded = prepareMemoryToolEnvelope({
|
|
51
117
|
error: error instanceof Error ? error.message : String(error),
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { bodyLimit } from 'hono/body-limit';
|
|
3
|
+
import { MAX_GROUP_MESSAGE_REQUEST_BYTES } from '../../shared/workspace-group-messages.js';
|
|
4
|
+
import { GroupMessageError, getGroupMessageBatch, startGroupMessageBatch, } from '../services/workspace-group-message-service.js';
|
|
5
|
+
const app = new Hono();
|
|
6
|
+
app.use('*', bodyLimit({ maxSize: MAX_GROUP_MESSAGE_REQUEST_BYTES }));
|
|
7
|
+
app.post('/', async (c) => {
|
|
8
|
+
const body = await c.req.json().catch(() => null);
|
|
9
|
+
try {
|
|
10
|
+
return c.json(startGroupMessageBatch(body), 202);
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
if (error instanceof GroupMessageError)
|
|
14
|
+
return c.json({ error: error.message }, error.status);
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
app.get('/:id', (c) => {
|
|
19
|
+
const batch = getGroupMessageBatch(c.req.param('id'));
|
|
20
|
+
return batch ? c.json(batch) : c.json({ error: 'Group message not found' }, 404);
|
|
21
|
+
});
|
|
22
|
+
export default app;
|
|
@@ -2,6 +2,8 @@ import { execFile as execFileCb, execFileSync, spawn } from 'node:child_process'
|
|
|
2
2
|
import { promisify } from 'node:util';
|
|
3
3
|
import { nanoid } from 'nanoid';
|
|
4
4
|
import { isWorkflowPolicy, resolveWorkflowPolicy, WORKFLOW_ACTIONS, } from '../../shared/workflow-policy.js';
|
|
5
|
+
import * as finalReviewService from '../services/auto-loop-final-review-service.js';
|
|
6
|
+
import { getReviewReturnStatus } from '../services/review-return-service.js';
|
|
5
7
|
import { getSearchIndexStatus, searchEvents } from '../services/search-service.js';
|
|
6
8
|
import { createSessionHandoff, decideSessionHandoff, getCurrentSessionHandoff, SessionHandoffError, submitSessionHandoff, waitForSessionHandoff, } from '../services/session-handoff-service.js';
|
|
7
9
|
import { AgentStopError, assertAgentStopped } from '../utils/agent-stop-result.js';
|
|
@@ -103,8 +105,16 @@ app.use('/:id/*', async (c, next) => {
|
|
|
103
105
|
}
|
|
104
106
|
await next();
|
|
105
107
|
});
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
+
// Branch/path mutations must also exclude teardown across awaits.
|
|
109
|
+
// Purge/delete/restore own their guard in the service.
|
|
110
|
+
for (const route of [
|
|
111
|
+
'/:id/archive',
|
|
112
|
+
'/:id/run-setup-script',
|
|
113
|
+
'/:id/cancel-source-change',
|
|
114
|
+
'/:id/rollback-file',
|
|
115
|
+
'/:id/rename-branch',
|
|
116
|
+
'/:id/resync-branch',
|
|
117
|
+
]) {
|
|
108
118
|
app.use(route, async (c, next) => {
|
|
109
119
|
try {
|
|
110
120
|
await withWorkspaceLifecycleGuard(c.req.param('id'), next);
|
|
@@ -466,6 +476,14 @@ app.post('/', migrationGuard, creationBodyLimit, async (c) => {
|
|
|
466
476
|
let setupScriptConfigured = false;
|
|
467
477
|
try {
|
|
468
478
|
const { body, attachments } = await readWorkspaceCreationRequest(c.req.raw);
|
|
479
|
+
if (body.autoLoopFinalReview !== undefined) {
|
|
480
|
+
try {
|
|
481
|
+
finalReviewService.parseFinalReviewConfiguration(body.autoLoopFinalReview);
|
|
482
|
+
}
|
|
483
|
+
catch (error) {
|
|
484
|
+
return c.json({ error: error instanceof Error ? error.message : 'Invalid final review configuration' }, 400);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
469
487
|
if (body.workflowPolicy !== undefined && !isWorkflowPolicy(body.workflowPolicy))
|
|
470
488
|
return c.json({ error: 'Invalid workflowPolicy' }, 400);
|
|
471
489
|
// workingBranch is derived from git when worktreePath is provided, so
|
|
@@ -560,6 +578,7 @@ app.post('/', migrationGuard, creationBodyLimit, async (c) => {
|
|
|
560
578
|
// the working branch from git itself — the body.workingBranch is ignored.
|
|
561
579
|
let useReusedWorktree = false;
|
|
562
580
|
let reusedDerivedBranch = null;
|
|
581
|
+
let reusedIdentity = null;
|
|
563
582
|
if (body.worktreePath) {
|
|
564
583
|
currentStep = 'inspect-worktree';
|
|
565
584
|
emitCreateProgress(creationId, currentStep);
|
|
@@ -588,6 +607,14 @@ app.post('/', migrationGuard, creationBodyLimit, async (c) => {
|
|
|
588
607
|
return c.json({ error: failure, step: 'inspect-worktree' }, 422);
|
|
589
608
|
}
|
|
590
609
|
reusedDerivedBranch = branch;
|
|
610
|
+
const directory = fs.statSync(body.worktreePath);
|
|
611
|
+
const gitEntry = fs.statSync(path.join(body.worktreePath, '.git'));
|
|
612
|
+
reusedIdentity = {
|
|
613
|
+
device: directory.dev,
|
|
614
|
+
inode: directory.ino,
|
|
615
|
+
gitDevice: gitEntry.dev,
|
|
616
|
+
gitInode: gitEntry.ino,
|
|
617
|
+
};
|
|
591
618
|
}
|
|
592
619
|
catch (err) {
|
|
593
620
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -771,6 +798,36 @@ app.post('/', migrationGuard, creationBodyLimit, async (c) => {
|
|
|
771
798
|
const creation = await withGitRepoLock(body.projectPath, async () => {
|
|
772
799
|
let prospectiveWorktreePath;
|
|
773
800
|
if (useReusedWorktree) {
|
|
801
|
+
// Imports above may yield long enough for a failed PR creation to remove
|
|
802
|
+
// its checkout. Recheck the exact directory and branch under the same
|
|
803
|
+
// repository lock as compensation before any record/context files exist.
|
|
804
|
+
try {
|
|
805
|
+
const worktreePath = body.worktreePath;
|
|
806
|
+
const directory = fs.statSync(worktreePath);
|
|
807
|
+
const gitEntry = fs.statSync(path.join(worktreePath, '.git'));
|
|
808
|
+
const commonDir = execFileSync('git', ['-C', worktreePath, 'rev-parse', '--path-format=absolute', '--git-common-dir'], {
|
|
809
|
+
encoding: 'utf-8',
|
|
810
|
+
timeout: 30_000,
|
|
811
|
+
}).trim();
|
|
812
|
+
const branch = execFileSync('git', ['-C', worktreePath, 'rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
813
|
+
encoding: 'utf-8',
|
|
814
|
+
timeout: 30_000,
|
|
815
|
+
}).trim();
|
|
816
|
+
if (!reusedIdentity ||
|
|
817
|
+
!directory.isDirectory() ||
|
|
818
|
+
directory.dev !== reusedIdentity.device ||
|
|
819
|
+
directory.ino !== reusedIdentity.inode ||
|
|
820
|
+
gitEntry.dev !== reusedIdentity.gitDevice ||
|
|
821
|
+
gitEntry.ino !== reusedIdentity.gitInode ||
|
|
822
|
+
realPathOrResolved(commonDir) !== realPathOrResolved(path.join(body.projectPath, '.git')) ||
|
|
823
|
+
branch !== reusedDerivedBranch)
|
|
824
|
+
throw new Error('The worktree directory, repository or branch changed during creation; retry from a fresh diagnosis');
|
|
825
|
+
}
|
|
826
|
+
catch (err) {
|
|
827
|
+
const failure = `Failed to revalidate worktree: ${err instanceof Error ? err.message : String(err)}`;
|
|
828
|
+
emitCreateFailed(creationId, 'inspect-worktree', failure);
|
|
829
|
+
return c.json({ error: failure, step: 'inspect-worktree' }, 422);
|
|
830
|
+
}
|
|
774
831
|
// Another attachment may have completed while this request waited for the lock.
|
|
775
832
|
const existing = getDb().prepare('SELECT id FROM workspaces WHERE worktree_path = ?').get(body.worktreePath);
|
|
776
833
|
if (existing) {
|
|
@@ -821,6 +878,8 @@ app.post('/', migrationGuard, creationBodyLimit, async (c) => {
|
|
|
821
878
|
...(useReusedWorktree ? {} : { worktreesPath: globalSettings.worktreesPath }),
|
|
822
879
|
});
|
|
823
880
|
createdWorkspace = workspace;
|
|
881
|
+
if (body.autoLoopFinalReview !== undefined)
|
|
882
|
+
finalReviewService.configureFinalReview(workspace.id, body.autoLoopFinalReview);
|
|
824
883
|
// Enable auto-loop before starting the initial brainstorming session so
|
|
825
884
|
// its model override is available when selecting that session's model.
|
|
826
885
|
if (body.autoLoop === true) {
|
|
@@ -1700,6 +1759,54 @@ app.get('/auto-loop-states', (c) => {
|
|
|
1700
1759
|
}
|
|
1701
1760
|
});
|
|
1702
1761
|
app.route('/', autoLoopMessagesRoutes);
|
|
1762
|
+
app.get('/:id/auto-loop/final-review', (c) => {
|
|
1763
|
+
const id = c.req.param('id');
|
|
1764
|
+
if (!workspaceService.getWorkspace(id))
|
|
1765
|
+
return c.json({ error: 'Workspace not found' }, 404);
|
|
1766
|
+
return c.json(finalReviewService.getFinalReviewStatus(id));
|
|
1767
|
+
});
|
|
1768
|
+
app.patch('/:id/auto-loop/final-review', migrationGuard, async (c) => {
|
|
1769
|
+
try {
|
|
1770
|
+
const body = await c.req.json();
|
|
1771
|
+
return c.json(finalReviewService.configureFinalReview(c.req.param('id'), body?.configuration));
|
|
1772
|
+
}
|
|
1773
|
+
catch (error) {
|
|
1774
|
+
return c.json({ error: error instanceof Error ? error.message : 'Invalid request' }, error instanceof ReviewRequestError ? error.status : 400);
|
|
1775
|
+
}
|
|
1776
|
+
});
|
|
1777
|
+
app.post('/:id/auto-loop/final-review/report', migrationGuard, async (c) => {
|
|
1778
|
+
try {
|
|
1779
|
+
const body = await c.req.json();
|
|
1780
|
+
return c.json(finalReviewService.submitFinalReviewReport(c.req.param('id'), body.sessionId, body.token, body.report));
|
|
1781
|
+
}
|
|
1782
|
+
catch (error) {
|
|
1783
|
+
return c.json({ error: error instanceof Error ? error.message : 'Invalid review report' }, error instanceof ReviewRequestError ? error.status : 400);
|
|
1784
|
+
}
|
|
1785
|
+
});
|
|
1786
|
+
app.get('/:id/review-return', (c) => {
|
|
1787
|
+
const id = c.req.param('id');
|
|
1788
|
+
if (!workspaceService.getWorkspace(id))
|
|
1789
|
+
return c.json({ error: 'Workspace not found' }, 404);
|
|
1790
|
+
return c.json(getReviewReturnStatus(id));
|
|
1791
|
+
});
|
|
1792
|
+
app.post('/:id/review-return/retry', migrationGuard, (c) => {
|
|
1793
|
+
try {
|
|
1794
|
+
agentManager.retryReviewReturn(c.req.param('id'));
|
|
1795
|
+
return c.json(getReviewReturnStatus(c.req.param('id')));
|
|
1796
|
+
}
|
|
1797
|
+
catch (error) {
|
|
1798
|
+
return c.json({ error: error instanceof Error ? error.message : 'Review recovery failed' }, 409);
|
|
1799
|
+
}
|
|
1800
|
+
});
|
|
1801
|
+
app.post('/:id/review-return/cancel', migrationGuard, (c) => {
|
|
1802
|
+
try {
|
|
1803
|
+
agentManager.cancelReviewReturn(c.req.param('id'));
|
|
1804
|
+
return c.json({ ok: true });
|
|
1805
|
+
}
|
|
1806
|
+
catch (error) {
|
|
1807
|
+
return c.json({ error: error instanceof Error ? error.message : 'Review cancellation failed' }, 409);
|
|
1808
|
+
}
|
|
1809
|
+
});
|
|
1703
1810
|
// GET /api/workspaces/:id/auto-loop — current auto-loop status for one workspace.
|
|
1704
1811
|
// Admission inspection never changes deadlines, pending schedules or lifecycle state.
|
|
1705
1812
|
app.get('/:id/automatic-admission', (c) => {
|
|
@@ -1979,7 +2086,7 @@ app.delete('/:id/sessions/:sessionId', migrationGuard, (c) => {
|
|
|
1979
2086
|
}
|
|
1980
2087
|
catch (err) {
|
|
1981
2088
|
const message = err instanceof Error ? err.message : String(err);
|
|
1982
|
-
return c.json({ error: message }, message.includes('active session') ? 409 : 500);
|
|
2089
|
+
return c.json({ error: message }, message.includes('active session') || err instanceof workspaceService.ReviewSessionInUseError ? 409 : 500);
|
|
1983
2090
|
}
|
|
1984
2091
|
});
|
|
1985
2092
|
// POST /api/workspaces/:id/deferred-tool-use/answer — resume a deferred
|
|
@@ -3120,6 +3227,12 @@ app.post('/:id/unarchive', migrationGuard, (c) => {
|
|
|
3120
3227
|
return c.json({ error: message }, workspaceErrorStatus(err));
|
|
3121
3228
|
}
|
|
3122
3229
|
});
|
|
3230
|
+
class WorkspaceBranchConfirmationError extends Error {
|
|
3231
|
+
code = 'branch-confirmation-mismatch';
|
|
3232
|
+
constructor() {
|
|
3233
|
+
super('The workspace branch changed or does not match confirmationBranch. Confirm its current branch before deleting.');
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
3123
3236
|
// Shared teardown for a single workspace: stops the agent, destroys the
|
|
3124
3237
|
// terminal, removes the owned worktree, optionally deletes local/remote
|
|
3125
3238
|
// branches, then deletes the DB row (cascades to tasks/sessions/events).
|
|
@@ -3131,6 +3244,8 @@ async function deleteWorkspaceWithSideEffects(workspace, opts) {
|
|
|
3131
3244
|
const current = workspaceService.getWorkspace(workspace.id);
|
|
3132
3245
|
if (!current)
|
|
3133
3246
|
throw new Error(`Workspace '${workspace.id}' not found`);
|
|
3247
|
+
if (opts.confirmationBranch !== undefined && opts.confirmationBranch !== current.workingBranch)
|
|
3248
|
+
throw new WorkspaceBranchConfirmationError();
|
|
3134
3249
|
// A bulk deletion may have captured the list before restoration completed.
|
|
3135
3250
|
if (workspace.archivedAt && !current.archivedAt) {
|
|
3136
3251
|
throw new Error('Workspace was unarchived while deletion was pending. Retry from its current state.');
|
|
@@ -3275,7 +3390,7 @@ app.delete('/:id', migrationGuard, async (c) => {
|
|
|
3275
3390
|
return c.json({ ok: true, warnings }, 200);
|
|
3276
3391
|
}
|
|
3277
3392
|
catch (err) {
|
|
3278
|
-
if (err instanceof WorkspaceLifecycleBusyError)
|
|
3393
|
+
if (err instanceof WorkspaceLifecycleBusyError || err instanceof WorkspaceBranchConfirmationError)
|
|
3279
3394
|
return c.json({ code: err.code, error: err.message }, 409);
|
|
3280
3395
|
const message = err instanceof Error ? err.message : String(err);
|
|
3281
3396
|
return c.json({ error: message }, workspaceErrorStatus(err));
|
|
@@ -75,8 +75,19 @@ class ClaudeInputStream {
|
|
|
75
75
|
closed = false;
|
|
76
76
|
queuedForcedMessages = 0;
|
|
77
77
|
unansweredMessages = new Set();
|
|
78
|
+
initialUuid;
|
|
79
|
+
initialYielded = false;
|
|
80
|
+
acceptInitial;
|
|
81
|
+
rejectInitial;
|
|
82
|
+
initialPromptAccepted = new Promise((resolve, reject) => {
|
|
83
|
+
this.acceptInitial = resolve;
|
|
84
|
+
this.rejectInitial = reject;
|
|
85
|
+
});
|
|
78
86
|
constructor(initialPrompt) {
|
|
79
|
-
|
|
87
|
+
const initial = this.toUserMessage(initialPrompt);
|
|
88
|
+
this.initialUuid = initial.uuid;
|
|
89
|
+
this.messages = [{ message: initial, forced: false }];
|
|
90
|
+
void this.initialPromptAccepted.catch(() => { });
|
|
80
91
|
}
|
|
81
92
|
send(text) {
|
|
82
93
|
if (this.closed)
|
|
@@ -91,6 +102,7 @@ class ClaudeInputStream {
|
|
|
91
102
|
wake?.();
|
|
92
103
|
}
|
|
93
104
|
close() {
|
|
105
|
+
this.rejectInitial(new Error('Claude ended before confirming consumption of the initial prompt'));
|
|
94
106
|
this.closed = true;
|
|
95
107
|
const wake = this.waiting;
|
|
96
108
|
this.waiting = undefined;
|
|
@@ -99,6 +111,19 @@ class ClaudeInputStream {
|
|
|
99
111
|
hasUnansweredInput() {
|
|
100
112
|
return this.queuedForcedMessages > 0 || this.unansweredMessages.size > 0;
|
|
101
113
|
}
|
|
114
|
+
/** Only provider-correlated foreground frames acknowledge this new prompt, never init/history/children. */
|
|
115
|
+
acknowledgeInitialInput(message) {
|
|
116
|
+
if (!this.initialYielded || ('parent_tool_use_id' in message && message.parent_tool_use_id != null))
|
|
117
|
+
return;
|
|
118
|
+
if (!['assistant', 'stream_event', 'result', 'user'].includes(message.type))
|
|
119
|
+
return;
|
|
120
|
+
const frame = message;
|
|
121
|
+
const ids = [...(frame.user_message_uuids ?? []), ...(frame.user_message_uuid ? [frame.user_message_uuid] : [])];
|
|
122
|
+
if (frame.type === 'user' && frame.uuid)
|
|
123
|
+
ids.push(frame.uuid);
|
|
124
|
+
if (ids.includes(this.initialUuid))
|
|
125
|
+
this.acceptInitial();
|
|
126
|
+
}
|
|
102
127
|
acknowledgeResult(result) {
|
|
103
128
|
// One Claude turn can consume several queued prompts. A result counts
|
|
104
129
|
// turns, not messages: only retire the sends explicitly covered by it.
|
|
@@ -133,6 +158,8 @@ class ClaudeInputStream {
|
|
|
133
158
|
if (next.forced)
|
|
134
159
|
this.queuedForcedMessages--;
|
|
135
160
|
this.unansweredMessages.add(next.message.uuid);
|
|
161
|
+
if (next.message.uuid === this.initialUuid)
|
|
162
|
+
this.initialYielded = true;
|
|
136
163
|
yield next.message;
|
|
137
164
|
continue;
|
|
138
165
|
}
|
|
@@ -229,6 +256,13 @@ export function createClaudeCodeEngine() {
|
|
|
229
256
|
// would approve ExitPlanMode and then every edit. Anything that needs a
|
|
230
257
|
// permission, a question included, is refused outright instead.
|
|
231
258
|
if (options.readOnly) {
|
|
259
|
+
// This capability-scoped report changes orchestration metadata only.
|
|
260
|
+
// The backend validates its token and current reviewer session again.
|
|
261
|
+
if (toolName === 'mcp__kobo-tasks__submit_final_review' &&
|
|
262
|
+
options.mcpServers?.some((server) => server.name === 'kobo-tasks' &&
|
|
263
|
+
server.env.KOBO_FINAL_REVIEW_TOKEN &&
|
|
264
|
+
server.env.KOBO_FINAL_REVIEW_SESSION_ID))
|
|
265
|
+
return Promise.resolve({ behavior: 'allow', updatedInput: input });
|
|
232
266
|
return Promise.resolve({
|
|
233
267
|
behavior: 'deny',
|
|
234
268
|
message: 'This is a read-only review session: do not modify anything or ask the user. Put the finding in your final report instead.',
|
|
@@ -677,6 +711,7 @@ export function createClaudeCodeEngine() {
|
|
|
677
711
|
turnLiveness.start();
|
|
678
712
|
try {
|
|
679
713
|
for await (const msg of q) {
|
|
714
|
+
inputStream.acknowledgeInitialInput(msg);
|
|
680
715
|
const events = mapSdkMessage(msg, mapperState);
|
|
681
716
|
const isForeground = !('parent_tool_use_id' in msg && msg.parent_tool_use_id != null);
|
|
682
717
|
const streamingContent = hasStreamingContent(msg);
|
|
@@ -867,6 +902,7 @@ export function createClaudeCodeEngine() {
|
|
|
867
902
|
})();
|
|
868
903
|
const engineProcess = {
|
|
869
904
|
ready,
|
|
905
|
+
initialPromptAccepted: inputStream.initialPromptAccepted,
|
|
870
906
|
closed: iteratorPromise.then(() => runtimeClosed),
|
|
871
907
|
get pid() {
|
|
872
908
|
return undefined;
|