@loicngr/kobo 1.6.11 → 1.6.13
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/README.md +10 -6
- package/dist/mcp-server/kobo-tasks-handlers.js +2 -2
- package/dist/server/db/migrations.js +31 -0
- package/dist/server/db/schema.js +11 -0
- package/dist/server/index.js +32 -5
- package/dist/server/routes/documents.js +2 -2
- package/dist/server/routes/git.js +21 -0
- package/dist/server/routes/health.js +2 -2
- package/dist/server/routes/images.js +3 -3
- package/dist/server/routes/usage.js +18 -0
- package/dist/server/routes/workspaces.js +231 -146
- package/dist/server/services/agent/engines/claude-code/args-builder.js +2 -0
- package/dist/server/services/agent/orchestrator.js +1 -1
- package/dist/server/services/auto-loop-service.js +22 -4
- package/dist/server/services/dev-server-service.js +2 -5
- package/dist/server/services/notion-service.js +15 -3
- package/dist/server/services/settings-service.js +18 -2
- package/dist/server/services/usage/db.js +29 -0
- package/dist/server/services/usage/index.js +2 -0
- package/dist/server/services/usage/poller.js +52 -0
- package/dist/server/services/usage/providers/claude-code.js +93 -0
- package/dist/server/services/usage/types.js +1 -0
- package/dist/server/services/wakeup-service.js +2 -2
- package/dist/server/services/websocket-service.js +14 -0
- package/dist/server/services/workspace-service.js +29 -3
- package/dist/server/services/worktree-service.js +50 -0
- package/dist/server/utils/mcp-client.js +7 -0
- package/dist/shared/auto-loop-prompts.js +46 -8
- package/package.json +1 -1
- package/src/client/dist/spa/assets/{ActivityFeed-Cv2cHAob.js → ActivityFeed-BsY3-q5d.js} +1 -1
- package/src/client/dist/spa/assets/CreatePage-Cdhkkx-X.js +2 -0
- package/src/client/dist/spa/assets/CreatePage-PRvhol1N.css +1 -0
- package/src/client/dist/spa/assets/{DiffViewer-xp8A1R-2.js → DiffViewer-DXcoEtVq.js} +2 -2
- package/src/client/dist/spa/assets/{HealthPage-iPEmaIxf.js → HealthPage-BSyGqDRu.js} +1 -1
- package/src/client/dist/spa/assets/MainLayout-D2SfvksB.css +1 -0
- package/src/client/dist/spa/assets/{MainLayout-CLSGgDp_.js → MainLayout-EYaLqjJx.js} +17 -17
- package/src/client/dist/spa/assets/{SearchPage-C_Z_-mPY.js → SearchPage-Bgx02GOH.js} +1 -1
- package/src/client/dist/spa/assets/SettingsPage-BTSOovDV.js +1 -0
- package/src/client/dist/spa/assets/SettingsPage-CwLELxfl.css +1 -0
- package/src/client/dist/spa/assets/WorkspacePage-C5MZx1sZ.css +1 -0
- package/src/client/dist/spa/assets/WorkspacePage-C8dJWu-n.js +4 -0
- package/src/client/dist/spa/assets/{build-path-tree-BAbslBF6.js → build-path-tree-D-2LpB2J.js} +1 -1
- package/src/client/dist/spa/assets/{cssMode-COLGl5q-.js → cssMode-DVBmJp-B.js} +1 -1
- package/src/client/dist/spa/assets/{documents-BlJv_G6j.js → documents-Ck8VwvpQ.js} +1 -1
- package/src/client/dist/spa/assets/{editor.api-CmWAkEBP.js → editor.api-DgbPJaK4.js} +1 -1
- package/src/client/dist/spa/assets/{editor.main-B50pvEsj.js → editor.main-BqqoRfAU.js} +3 -3
- package/src/client/dist/spa/assets/{expand-template-CVF0qYFz.js → expand-template-bkCTc78P.js} +1 -1
- package/src/client/dist/spa/assets/{freemarker2-x6p1HdGv.js → freemarker2-CgaW0Q0y.js} +1 -1
- package/src/client/dist/spa/assets/{handlebars-DVdpHx-F.js → handlebars-BSs5PdXe.js} +1 -1
- package/src/client/dist/spa/assets/{html-CVBXIMk9.js → html-C9wlJaMs.js} +1 -1
- package/src/client/dist/spa/assets/{htmlMode-bcy_B_7M.js → htmlMode-DaRssGJk.js} +1 -1
- package/src/client/dist/spa/assets/i18n-BSNIShFg.js +1 -0
- package/src/client/dist/spa/assets/index-odgA9x8A.js +2 -0
- package/src/client/dist/spa/assets/{javascript-C8NntD-t.js → javascript-D0VYhsc-.js} +1 -1
- package/src/client/dist/spa/assets/{jsonMode-CKmO44kP.js → jsonMode-B57EaUNS.js} +1 -1
- package/src/client/dist/spa/assets/kobo-commands-D-9dbM70.js +11 -0
- package/src/client/dist/spa/assets/{liquid-JGbytnvM.js → liquid-gP2gg7sw.js} +1 -1
- package/src/client/dist/spa/assets/{mdx-8xDuI4Ra.js → mdx-HhXcZn_S.js} +1 -1
- package/src/client/dist/spa/assets/{models-IFgNVQuG.js → models-CJC61gWE.js} +1 -1
- package/src/client/dist/spa/assets/{monaco.contribution-DxgwWnvV.js → monaco.contribution-ChJg8bwd.js} +2 -2
- package/src/client/dist/spa/assets/{python-DntkvJJn.js → python-DM6FfMV3.js} +1 -1
- package/src/client/dist/spa/assets/{razor-CBMu7MSu.js → razor-XifsxhTG.js} +1 -1
- package/src/client/dist/spa/assets/stats-C3n1k51k.js +1 -0
- package/src/client/dist/spa/assets/{tsMode-qUgDyCdk.js → tsMode-B8gurPqG.js} +1 -1
- package/src/client/dist/spa/assets/{typescript-Bp6APJ3s.js → typescript-CZKTCOjl.js} +1 -1
- package/src/client/dist/spa/assets/{xml-D1_1t5sz.js → xml-CtZPkb7Q.js} +1 -1
- package/src/client/dist/spa/assets/{yaml-T38tRjC8.js → yaml-D5IEE5M-.js} +1 -1
- package/src/client/dist/spa/index.html +1 -1
- package/src/mcp-server/kobo-tasks-handlers.ts +3 -2
- package/src/client/dist/spa/assets/CreatePage-DrGARGo5.js +0 -2
- package/src/client/dist/spa/assets/CreatePage-d0Qp-PnO.css +0 -1
- package/src/client/dist/spa/assets/MainLayout-CDR4Le5c.css +0 -1
- package/src/client/dist/spa/assets/SettingsPage-CLMCHMpz.css +0 -1
- package/src/client/dist/spa/assets/SettingsPage-DETFZXCZ.js +0 -1
- package/src/client/dist/spa/assets/WorkspacePage-BAkCj4gZ.js +0 -4
- package/src/client/dist/spa/assets/WorkspacePage-Bo1GW3wo.css +0 -1
- package/src/client/dist/spa/assets/i18n-DncqzfKK.js +0 -1
- package/src/client/dist/spa/assets/index-Dl8rTFls.js +0 -2
- package/src/client/dist/spa/assets/kobo-commands-30GNdCpd.js +0 -10
- package/src/client/dist/spa/assets/rate-limit-labels-BaD9dQtl.js +0 -1
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { execFile as execFileCb, spawn } from 'node:child_process';
|
|
1
|
+
import { execFile as execFileCb, execFileSync, spawn } from 'node:child_process';
|
|
2
2
|
import { promisify } from 'node:util';
|
|
3
3
|
const execFileAsync = promisify(execFileCb);
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { Hono } from 'hono';
|
|
7
|
-
import {
|
|
7
|
+
import { AUTO_LOOP_HARD_RULES, buildAutoLoopGroomingSteps, PREP_AUTOLOOP_INTRO, } from '../../shared/auto-loop-prompts.js';
|
|
8
8
|
import { getDb } from '../db/index.js';
|
|
9
9
|
import { migrationGuard } from '../middleware/migration-guard.js';
|
|
10
10
|
import { listEngines } from '../services/agent/engines/registry.js';
|
|
@@ -41,8 +41,13 @@ app.get('/', (c) => {
|
|
|
41
41
|
app.post('/', migrationGuard, async (c) => {
|
|
42
42
|
try {
|
|
43
43
|
const body = await c.req.json();
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
// workingBranch is derived from git when worktreePath is provided, so
|
|
45
|
+
// it's not required in that flow. The other 3 fields stay mandatory.
|
|
46
|
+
if (!body.name || !body.projectPath || !body.sourceBranch) {
|
|
47
|
+
return c.json({ error: 'Missing required fields: name, projectPath, sourceBranch' }, 400);
|
|
48
|
+
}
|
|
49
|
+
if (!body.worktreePath && !body.workingBranch) {
|
|
50
|
+
return c.json({ error: 'Missing required field: workingBranch' }, 400);
|
|
46
51
|
}
|
|
47
52
|
// Validate the engine id (if provided) against the registry. An unknown
|
|
48
53
|
// engine is rejected up-front so we don't create orphan workspaces that
|
|
@@ -62,10 +67,100 @@ app.post('/', migrationGuard, async (c) => {
|
|
|
62
67
|
const message = err instanceof Error ? err.message : String(err);
|
|
63
68
|
return c.json({ error: message }, 422);
|
|
64
69
|
}
|
|
70
|
+
// Reuse-existing-worktree path. When the caller passes `worktreePath`,
|
|
71
|
+
// Kobo "attaches" to a pre-existing worktree on disk instead of creating
|
|
72
|
+
// a new one. We validate four invariants up-front (path exists, belongs
|
|
73
|
+
// to this repo, is on a real branch, isn't already attached) and derive
|
|
74
|
+
// the working branch from git itself — the body.workingBranch is ignored.
|
|
75
|
+
let useReusedWorktree = false;
|
|
76
|
+
let reusedDerivedBranch = null;
|
|
77
|
+
if (body.worktreePath) {
|
|
78
|
+
if (!fs.existsSync(body.worktreePath)) {
|
|
79
|
+
return c.json({ error: `Worktree path does not exist: ${body.worktreePath}` }, 422);
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const commonDir = execFileSync('git', ['-C', body.worktreePath, 'rev-parse', '--git-common-dir'], {
|
|
83
|
+
encoding: 'utf-8',
|
|
84
|
+
}).trim();
|
|
85
|
+
const expectedCommonDir = path.join(body.projectPath, '.git');
|
|
86
|
+
if (path.resolve(commonDir) !== path.resolve(expectedCommonDir)) {
|
|
87
|
+
return c.json({ error: `Worktree '${body.worktreePath}' belongs to a different repository` }, 422);
|
|
88
|
+
}
|
|
89
|
+
const branch = execFileSync('git', ['-C', body.worktreePath, 'rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
90
|
+
encoding: 'utf-8',
|
|
91
|
+
}).trim();
|
|
92
|
+
if (!branch || branch === 'HEAD') {
|
|
93
|
+
return c.json({ error: 'Worktree is in detached HEAD state and cannot be attached' }, 422);
|
|
94
|
+
}
|
|
95
|
+
reusedDerivedBranch = branch;
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
99
|
+
return c.json({ error: `Failed to inspect worktree: ${message}` }, 422);
|
|
100
|
+
}
|
|
101
|
+
// Validate the worktree isn't already attached to another workspace.
|
|
102
|
+
const dbForCheck = getDb();
|
|
103
|
+
const existing = dbForCheck.prepare('SELECT id FROM workspaces WHERE worktree_path = ?').get(body.worktreePath);
|
|
104
|
+
if (existing) {
|
|
105
|
+
return c.json({ error: 'This worktree is already attached to another Kōbō workspace' }, 422);
|
|
106
|
+
}
|
|
107
|
+
useReusedWorktree = true;
|
|
108
|
+
}
|
|
109
|
+
// Pre-flight: extract Notion / Sentry before any DB write. A throw here
|
|
110
|
+
// must not leave a half-built workspace behind, so we run extraction
|
|
111
|
+
// before createWorkspace and surface failures as 422.
|
|
112
|
+
let notionContent = null;
|
|
113
|
+
if (body.notionUrl) {
|
|
114
|
+
try {
|
|
115
|
+
notionContent = await notionService.extractNotionPage(body.notionUrl);
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
119
|
+
return c.json({ error: `Failed to extract Notion page: ${message}` }, 422);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
let sentryContent = null;
|
|
123
|
+
if (body.sentryUrl) {
|
|
124
|
+
try {
|
|
125
|
+
sentryContent = await sentryService.extractSentryIssue(body.sentryUrl);
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
129
|
+
return c.json({ error: `Failed to extract Sentry issue: ${message}` }, 422);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
65
132
|
// Create workspace record
|
|
66
133
|
const globalSettings = settingsService.getGlobalSettings();
|
|
67
|
-
// workingBranch may be updated after Notion extraction to inject the ticket ID
|
|
68
|
-
|
|
134
|
+
// workingBranch may be updated after Notion extraction to inject the ticket ID,
|
|
135
|
+
// OR overridden by the branch derived from the existing worktree (reuse mode).
|
|
136
|
+
let workingBranch = useReusedWorktree && reusedDerivedBranch ? reusedDerivedBranch : body.workingBranch;
|
|
137
|
+
// Inject ticket ID into the working branch BEFORE creating the workspace,
|
|
138
|
+
// so the worktree_path recorded in the DB reflects the FINAL branch name.
|
|
139
|
+
// Works with or without Notion: ticket ID comes from Notion extraction first,
|
|
140
|
+
// then Sentry, then falls back to a TK-XXXX pattern anywhere in the body.name.
|
|
141
|
+
// Skip when reusing an existing worktree — its branch is already real on disk
|
|
142
|
+
// and we MUST NOT rename it.
|
|
143
|
+
if (!useReusedWorktree) {
|
|
144
|
+
// Sentry's canonical identifier is the issue short-ID (e.g. "ACME-API-3"),
|
|
145
|
+
// which is what Sentry auto-close recognises in commit messages.
|
|
146
|
+
const detectedTicketId = notionContent?.ticketId || sentryContent?.issueId || body.name.match(/[A-Z]+-\d+/i)?.[0];
|
|
147
|
+
if (detectedTicketId && !workingBranch.toLowerCase().includes(detectedTicketId.toLowerCase())) {
|
|
148
|
+
const ticketPrefix = detectedTicketId.toUpperCase();
|
|
149
|
+
const slashIdx = workingBranch.indexOf('/');
|
|
150
|
+
const typePrefix = slashIdx >= 0 ? workingBranch.slice(0, slashIdx + 1) : 'feature/';
|
|
151
|
+
// Use Notion/Sentry title or body name for the slug — all have proper accented
|
|
152
|
+
// characters that NFD normalization can transliterate (é→e, ç→c, etc.)
|
|
153
|
+
const titleSource = notionContent?.title || sentryContent?.title || body.name;
|
|
154
|
+
const titleSlug = titleSource
|
|
155
|
+
.normalize('NFD')
|
|
156
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
157
|
+
.toLowerCase()
|
|
158
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
159
|
+
.replace(/^-|-$/g, '')
|
|
160
|
+
.substring(0, 50);
|
|
161
|
+
workingBranch = `${typePrefix}${ticketPrefix}--${titleSlug}`;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
69
164
|
let workspace = workspaceService.createWorkspace({
|
|
70
165
|
name: body.name,
|
|
71
166
|
projectPath: body.projectPath,
|
|
@@ -73,13 +168,13 @@ app.post('/', migrationGuard, async (c) => {
|
|
|
73
168
|
workingBranch,
|
|
74
169
|
notionUrl: body.notionUrl,
|
|
75
170
|
notionPageId: body.notionPageId,
|
|
171
|
+
sentryUrl: body.sentryUrl,
|
|
172
|
+
...(useReusedWorktree ? { worktreePath: body.worktreePath, worktreeOwned: false } : {}),
|
|
76
173
|
model: body.model,
|
|
77
174
|
reasoningEffort: body.reasoningEffort,
|
|
78
175
|
permissionMode: body.permissionMode || globalSettings.defaultPermissionMode || 'plan',
|
|
79
176
|
engine: body.engine,
|
|
80
177
|
});
|
|
81
|
-
let notionContent = null;
|
|
82
|
-
let sentryContent = null;
|
|
83
178
|
// Auto-tag the workspace based on its creation source — `notion` when
|
|
84
179
|
// imported from a Notion page, `sentry` when bootstrapped from a Sentry
|
|
85
180
|
// issue URL. Pre-seeded in the global tag catalogue via migration v9.
|
|
@@ -101,29 +196,6 @@ app.post('/', migrationGuard, async (c) => {
|
|
|
101
196
|
console.error('[workspaces] Failed to apply auto tags:', err);
|
|
102
197
|
}
|
|
103
198
|
}
|
|
104
|
-
// Extract Notion page content if a URL was provided
|
|
105
|
-
if (body.notionUrl) {
|
|
106
|
-
workspaceService.updateWorkspaceStatus(workspace.id, 'extracting');
|
|
107
|
-
try {
|
|
108
|
-
notionContent = await notionService.extractNotionPage(body.notionUrl);
|
|
109
|
-
}
|
|
110
|
-
catch (err) {
|
|
111
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
112
|
-
console.error(`[workspaces] Failed to extract Notion page: ${message}`);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
// Extract Sentry issue content if a URL was provided. Done early (before
|
|
116
|
-
// worktree creation) so the issue ID can be injected into the branch name.
|
|
117
|
-
if (body.sentryUrl) {
|
|
118
|
-
workspaceService.updateWorkspaceStatus(workspace.id, 'extracting');
|
|
119
|
-
try {
|
|
120
|
-
sentryContent = await sentryService.extractSentryIssue(body.sentryUrl);
|
|
121
|
-
}
|
|
122
|
-
catch (err) {
|
|
123
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
124
|
-
console.error(`[workspaces] Failed to extract Sentry issue: ${message}`);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
199
|
// Update workspace name with Sentry issue title if the user did not provide
|
|
128
200
|
// a custom name and Notion hasn't already filled it.
|
|
129
201
|
if (sentryContent?.title && !notionContent?.title && workspace.name === 'workspace') {
|
|
@@ -177,41 +249,21 @@ app.post('/', migrationGuard, async (c) => {
|
|
|
177
249
|
}
|
|
178
250
|
}
|
|
179
251
|
}
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
// then falls back to a TK-XXXX pattern anywhere in the workspace name.
|
|
183
|
-
// The worktree has not been created yet, so a DB update is sufficient.
|
|
184
|
-
{
|
|
185
|
-
// Sentry's canonical identifier is the issue short-ID (e.g. "ACME-API-3"),
|
|
186
|
-
// which is what Sentry auto-close recognises in commit messages.
|
|
187
|
-
const detectedTicketId = notionContent?.ticketId || sentryContent?.issueId || workspace.name.match(/[A-Z]+-\d+/i)?.[0];
|
|
188
|
-
if (detectedTicketId && !workingBranch.toLowerCase().includes(detectedTicketId.toLowerCase())) {
|
|
189
|
-
const ticketPrefix = detectedTicketId.toUpperCase();
|
|
190
|
-
const slashIdx = workingBranch.indexOf('/');
|
|
191
|
-
const typePrefix = slashIdx >= 0 ? workingBranch.slice(0, slashIdx + 1) : 'feature/';
|
|
192
|
-
// Use Notion/Sentry title or workspace name for the slug — all have proper accented
|
|
193
|
-
// characters that NFD normalization can transliterate (é→e, ç→c, etc.)
|
|
194
|
-
const titleSource = notionContent?.title || sentryContent?.title || workspace.name;
|
|
195
|
-
const titleSlug = titleSource
|
|
196
|
-
.normalize('NFD')
|
|
197
|
-
.replace(/[\u0300-\u036f]/g, '')
|
|
198
|
-
.toLowerCase()
|
|
199
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
200
|
-
.replace(/^-|-$/g, '')
|
|
201
|
-
.substring(0, 50);
|
|
202
|
-
workingBranch = `${typePrefix}${ticketPrefix}--${titleSlug}`;
|
|
203
|
-
workspace = workspaceService.updateWorkingBranch(workspace.id, workingBranch);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
// Create git worktree for the working branch
|
|
252
|
+
// Create git worktree for the working branch — unless we're reusing an
|
|
253
|
+
// existing one, in which case the path is taken straight from the body.
|
|
207
254
|
let worktreePath;
|
|
208
|
-
|
|
209
|
-
worktreePath =
|
|
255
|
+
if (useReusedWorktree) {
|
|
256
|
+
worktreePath = body.worktreePath;
|
|
210
257
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
258
|
+
else {
|
|
259
|
+
try {
|
|
260
|
+
worktreePath = worktreeService.createWorktree(body.projectPath, workingBranch, body.sourceBranch);
|
|
261
|
+
}
|
|
262
|
+
catch (err) {
|
|
263
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
264
|
+
workspaceService.updateWorkspaceStatus(workspace.id, 'error');
|
|
265
|
+
return c.json({ error: `Failed to create worktree: ${message}` }, 500);
|
|
266
|
+
}
|
|
215
267
|
}
|
|
216
268
|
// Ensure Kobo-generated files are gitignored. Check both the root
|
|
217
269
|
// .gitignore and .ai/.gitignore to avoid duplicate entries.
|
|
@@ -260,7 +312,10 @@ app.post('/', migrationGuard, async (c) => {
|
|
|
260
312
|
}
|
|
261
313
|
// Run setup script if configured and not skipped
|
|
262
314
|
let setupScriptFailed = false;
|
|
263
|
-
|
|
315
|
+
// Skip the setup script when reusing an existing worktree — the user
|
|
316
|
+
// already has the environment set up there and rerunning it could be
|
|
317
|
+
// destructive (drop a node_modules they curated, etc.).
|
|
318
|
+
if (effectiveSettings.setupScript && !body.skipSetupScript && !useReusedWorktree) {
|
|
264
319
|
workspaceService.updateWorkspaceStatus(workspace.id, 'extracting');
|
|
265
320
|
wsService.emit(workspace.id, 'setup:output', { text: '[kobo] Running setup script...' });
|
|
266
321
|
try {
|
|
@@ -460,12 +515,20 @@ app.post('/', migrationGuard, async (c) => {
|
|
|
460
515
|
// NOT with implementation. The auto-loop will drive implementation after.
|
|
461
516
|
// The grooming steps + hard rules are shared with the PREP_AUTOLOOP_PROMPT
|
|
462
517
|
// sent by the "Prepare for auto-loop" button (src/shared/auto-loop-prompts.ts).
|
|
518
|
+
// Read per-project E2E settings so the grooming steps can include the
|
|
519
|
+
// E2E review pass when configured. We deliberately use
|
|
520
|
+
// `getProjectSettings` (NOT `getEffectiveSettings`) here because only
|
|
521
|
+
// project-level settings carry the `e2e` shape; if the project hasn't
|
|
522
|
+
// been registered yet, the empty default below is correct.
|
|
523
|
+
const projectSettingsForE2e = settingsService.getProjectSettings(body.projectPath);
|
|
524
|
+
const e2eSettings = projectSettingsForE2e?.e2e ?? { framework: '', skill: '', prompt: '' };
|
|
463
525
|
brainstormPrompt += `\n\nThen brainstorm the implementation approach. Explore the codebase to understand the existing structure. Ask clarifying questions if needed. When you have a clear plan, create a plan file.
|
|
464
526
|
|
|
465
527
|
Auto-loop mode is active for this workspace. After the plan is ready, DO NOT implement anything. Instead:
|
|
466
528
|
|
|
467
|
-
${
|
|
468
|
-
|
|
529
|
+
${buildAutoLoopGroomingSteps(e2eSettings)}
|
|
530
|
+
|
|
531
|
+
When the steps above are complete, output [BRAINSTORM_COMPLETE] on its own line and end your turn cleanly.
|
|
469
532
|
|
|
470
533
|
${AUTO_LOOP_HARD_RULES}`;
|
|
471
534
|
}
|
|
@@ -688,46 +751,6 @@ app.patch('/:id/sessions/:sessionId', async (c) => {
|
|
|
688
751
|
return c.json({ error: message }, 500);
|
|
689
752
|
}
|
|
690
753
|
});
|
|
691
|
-
// POST /api/workspaces/:id/refresh-notion — re-extract Notion page and update tasks
|
|
692
|
-
app.post('/:id/refresh-notion', async (c) => {
|
|
693
|
-
try {
|
|
694
|
-
const id = c.req.param('id');
|
|
695
|
-
const workspace = workspaceService.getWorkspace(id);
|
|
696
|
-
if (!workspace)
|
|
697
|
-
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
698
|
-
if (!workspace.notionUrl)
|
|
699
|
-
return c.json({ error: 'No Notion URL configured' }, 400);
|
|
700
|
-
const notionContent = await notionService.extractNotionPage(workspace.notionUrl);
|
|
701
|
-
// Delete existing tasks and recreate from Notion
|
|
702
|
-
const db = getDb();
|
|
703
|
-
db.prepare('DELETE FROM tasks WHERE workspace_id = ?').run(id);
|
|
704
|
-
let sortOrder = 0;
|
|
705
|
-
for (const todo of notionContent.todos) {
|
|
706
|
-
workspaceService.createTask(id, {
|
|
707
|
-
title: todo.title,
|
|
708
|
-
isAcceptanceCriterion: false,
|
|
709
|
-
sortOrder: sortOrder++,
|
|
710
|
-
});
|
|
711
|
-
}
|
|
712
|
-
for (const feature of notionContent.gherkinFeatures) {
|
|
713
|
-
workspaceService.createTask(id, {
|
|
714
|
-
title: feature,
|
|
715
|
-
isAcceptanceCriterion: true,
|
|
716
|
-
sortOrder: sortOrder++,
|
|
717
|
-
});
|
|
718
|
-
}
|
|
719
|
-
// Update name if it was the default
|
|
720
|
-
if (notionContent.title && workspace.name === 'workspace') {
|
|
721
|
-
workspaceService.updateWorkspaceName(id, notionContent.title);
|
|
722
|
-
}
|
|
723
|
-
const updated = workspaceService.getWorkspaceWithTasks(id);
|
|
724
|
-
return c.json(updated);
|
|
725
|
-
}
|
|
726
|
-
catch (err) {
|
|
727
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
728
|
-
return c.json({ error: message }, 500);
|
|
729
|
-
}
|
|
730
|
-
});
|
|
731
754
|
// POST /api/workspaces/:id/tasks — create a new task
|
|
732
755
|
app.post('/:id/tasks', async (c) => {
|
|
733
756
|
try {
|
|
@@ -949,6 +972,29 @@ app.get('/archived', (c) => {
|
|
|
949
972
|
return c.json({ error: message }, 500);
|
|
950
973
|
}
|
|
951
974
|
});
|
|
975
|
+
// GET /:id/prep-autoloop-prompt — compose the project-aware grooming
|
|
976
|
+
// prompt. Used by the "Prepare for auto-loop" button. Place BEFORE
|
|
977
|
+
// `app.get('/:id', ...)` so the more-specific path wins.
|
|
978
|
+
app.get('/:id/prep-autoloop-prompt', (c) => {
|
|
979
|
+
try {
|
|
980
|
+
const id = c.req.param('id');
|
|
981
|
+
const workspace = workspaceService.getWorkspace(id);
|
|
982
|
+
if (!workspace)
|
|
983
|
+
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
984
|
+
const projectSettings = settingsService.getProjectSettings(workspace.projectPath);
|
|
985
|
+
const e2eSettings = projectSettings?.e2e ?? { framework: '', skill: '', prompt: '' };
|
|
986
|
+
const prompt = `${PREP_AUTOLOOP_INTRO}
|
|
987
|
+
|
|
988
|
+
${buildAutoLoopGroomingSteps(e2eSettings)}
|
|
989
|
+
|
|
990
|
+
${AUTO_LOOP_HARD_RULES}`;
|
|
991
|
+
return c.json({ prompt });
|
|
992
|
+
}
|
|
993
|
+
catch (err) {
|
|
994
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
995
|
+
return c.json({ error: message }, 500);
|
|
996
|
+
}
|
|
997
|
+
});
|
|
952
998
|
// GET /api/workspaces/:id — get workspace details with tasks
|
|
953
999
|
app.get('/:id', (c) => {
|
|
954
1000
|
try {
|
|
@@ -1079,7 +1125,7 @@ app.post('/:id/open-editor', (c) => {
|
|
|
1079
1125
|
if (!globalSettings.editorCommand) {
|
|
1080
1126
|
return c.json({ error: 'No editor command configured' }, 400);
|
|
1081
1127
|
}
|
|
1082
|
-
const worktreePath =
|
|
1128
|
+
const worktreePath = workspace.worktreePath;
|
|
1083
1129
|
if (!fs.existsSync(worktreePath)) {
|
|
1084
1130
|
return c.json({ error: `Worktree path does not exist: ${worktreePath}` }, 400);
|
|
1085
1131
|
}
|
|
@@ -1087,6 +1133,15 @@ app.post('/:id/open-editor', (c) => {
|
|
|
1087
1133
|
detached: true,
|
|
1088
1134
|
stdio: 'ignore',
|
|
1089
1135
|
});
|
|
1136
|
+
// spawn errors fire async on the ChildProcess (ENOENT etc.) — without a
|
|
1137
|
+
// handler the unhandled 'error' event crashes the whole Node process.
|
|
1138
|
+
child.on('error', (err) => {
|
|
1139
|
+
console.error(`[open-editor] spawn '${globalSettings.editorCommand}' failed:`, err.message);
|
|
1140
|
+
wsService.emitEphemeral(workspace.id, 'editor:open-failed', {
|
|
1141
|
+
command: globalSettings.editorCommand,
|
|
1142
|
+
message: err.message,
|
|
1143
|
+
});
|
|
1144
|
+
});
|
|
1090
1145
|
child.unref();
|
|
1091
1146
|
return c.json({ success: true });
|
|
1092
1147
|
}
|
|
@@ -1118,7 +1173,7 @@ app.post('/:id/run-setup-script', async (c) => {
|
|
|
1118
1173
|
if (!effectiveSettings.setupScript) {
|
|
1119
1174
|
return c.json({ error: 'No setup script configured' }, 400);
|
|
1120
1175
|
}
|
|
1121
|
-
const worktreePath =
|
|
1176
|
+
const worktreePath = workspace.worktreePath;
|
|
1122
1177
|
if (!fs.existsSync(worktreePath)) {
|
|
1123
1178
|
return c.json({ error: `Worktree path does not exist: ${worktreePath}` }, 400);
|
|
1124
1179
|
}
|
|
@@ -1235,20 +1290,25 @@ app.delete('/:id', migrationGuard, async (c) => {
|
|
|
1235
1290
|
// Docker leaves root-owned files inside the worktree, git worktree
|
|
1236
1291
|
// remove fails with EACCES.
|
|
1237
1292
|
const warnings = [];
|
|
1238
|
-
// Remove worktree
|
|
1239
|
-
|
|
1240
|
-
const worktreePath =
|
|
1241
|
-
|
|
1242
|
-
|
|
1293
|
+
// Remove worktree (only if owned — for attached external worktrees we
|
|
1294
|
+
// never created the dir, so we must not delete it on the user's behalf).
|
|
1295
|
+
const worktreePath = workspace.worktreePath;
|
|
1296
|
+
if (workspace.worktreeOwned) {
|
|
1297
|
+
try {
|
|
1298
|
+
worktreeService.removeWorktree(workspace.projectPath, worktreePath);
|
|
1299
|
+
}
|
|
1300
|
+
catch (err) {
|
|
1301
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1302
|
+
console.error(`[workspaces] Failed to remove worktree: ${message}`);
|
|
1303
|
+
warnings.push(`Failed to remove worktree directory '${worktreePath}'. The git entry may still reference it. ` +
|
|
1304
|
+
`Fix manually:\n` +
|
|
1305
|
+
` sudo rm -rf '${worktreePath}'\n` +
|
|
1306
|
+
` cd '${workspace.projectPath}' && git worktree prune\n` +
|
|
1307
|
+
`Reason: ${message}`);
|
|
1308
|
+
}
|
|
1243
1309
|
}
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
console.error(`[workspaces] Failed to remove worktree: ${message}`);
|
|
1247
|
-
warnings.push(`Failed to remove worktree directory '${worktreePath}'. The git entry may still reference it. ` +
|
|
1248
|
-
`Fix manually:\n` +
|
|
1249
|
-
` sudo rm -rf '${worktreePath}'\n` +
|
|
1250
|
-
` cd '${workspace.projectPath}' && git worktree prune\n` +
|
|
1251
|
-
`Reason: ${message}`);
|
|
1310
|
+
else {
|
|
1311
|
+
console.log(`[workspaces] keeping reused worktree on delete: ${worktreePath}`);
|
|
1252
1312
|
}
|
|
1253
1313
|
// Delete local branch if requested
|
|
1254
1314
|
if (body.deleteLocalBranch) {
|
|
@@ -1324,7 +1384,7 @@ app.post('/:id/start', migrationGuard, async (c) => {
|
|
|
1324
1384
|
catch {
|
|
1325
1385
|
// Agent may not be running — ignore
|
|
1326
1386
|
}
|
|
1327
|
-
const worktreePath =
|
|
1387
|
+
const worktreePath = workspace.worktreePath;
|
|
1328
1388
|
const agent = agentManager.startAgent(id, worktreePath, prompt, workspace.model, resume, workspace.permissionMode, agentSessionId, workspace.reasoningEffort);
|
|
1329
1389
|
workspaceService.updateWorkspaceStatus(id, 'executing');
|
|
1330
1390
|
// Persist the user prompt so it survives page refresh.
|
|
@@ -1348,7 +1408,7 @@ app.get('/:id/git-stats', async (c) => {
|
|
|
1348
1408
|
if (!workspace) {
|
|
1349
1409
|
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
1350
1410
|
}
|
|
1351
|
-
const worktreePath =
|
|
1411
|
+
const worktreePath = workspace.worktreePath;
|
|
1352
1412
|
const commitCount = gitOps.getCommitCount(worktreePath, workspace.sourceBranch, workspace.workingBranch);
|
|
1353
1413
|
const diffStats = gitOps.getStructuredDiffStatsBetween(worktreePath, workspace.sourceBranch, workspace.workingBranch);
|
|
1354
1414
|
const pr = await gitOps.getPrStatusAsync(workspace.projectPath, workspace.workingBranch);
|
|
@@ -1383,7 +1443,7 @@ app.get('/:id/diff', (c) => {
|
|
|
1383
1443
|
if (!workspace) {
|
|
1384
1444
|
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
1385
1445
|
}
|
|
1386
|
-
const worktreePath =
|
|
1446
|
+
const worktreePath = workspace.worktreePath;
|
|
1387
1447
|
const files = mode === 'unpushed'
|
|
1388
1448
|
? gitOps.getUnpushedChangedFiles(worktreePath, workspace.workingBranch)
|
|
1389
1449
|
: gitOps.getChangedFiles(worktreePath, workspace.sourceBranch);
|
|
@@ -1417,7 +1477,7 @@ app.get('/:id/diff-file', (c) => {
|
|
|
1417
1477
|
if (!workspace) {
|
|
1418
1478
|
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
1419
1479
|
}
|
|
1420
|
-
const worktreePath =
|
|
1480
|
+
const worktreePath = workspace.worktreePath;
|
|
1421
1481
|
const baseRef = mode === 'unpushed' ? `origin/${workspace.workingBranch}` : workspace.sourceBranch;
|
|
1422
1482
|
const original = gitOps.getFileAtRef(worktreePath, baseRef, filePath);
|
|
1423
1483
|
const modified = gitOps.getFileContent(worktreePath, filePath);
|
|
@@ -1440,7 +1500,7 @@ app.get('/:id/commits', (c) => {
|
|
|
1440
1500
|
}
|
|
1441
1501
|
const limitRaw = c.req.query('limit');
|
|
1442
1502
|
const limit = Math.min(Math.max(1, parseInt(limitRaw ?? '50', 10) || 50), 200);
|
|
1443
|
-
const worktreePath =
|
|
1503
|
+
const worktreePath = workspace.worktreePath;
|
|
1444
1504
|
const commits = gitOps.listBranchCommits(worktreePath, workspace.sourceBranch, workspace.workingBranch, limit);
|
|
1445
1505
|
c.header('Cache-Control', 'no-store');
|
|
1446
1506
|
return c.json({ commits, sourceBranch: workspace.sourceBranch, workingBranch: workspace.workingBranch });
|
|
@@ -1470,11 +1530,23 @@ app.post('/:id/rename-branch', async (c) => {
|
|
|
1470
1530
|
if (!workspace) {
|
|
1471
1531
|
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
1472
1532
|
}
|
|
1533
|
+
if (!workspace.worktreeOwned) {
|
|
1534
|
+
return c.json({
|
|
1535
|
+
error: 'Rename is not available for attached external worktrees. Manage the branch name with git directly.',
|
|
1536
|
+
}, 400);
|
|
1537
|
+
}
|
|
1473
1538
|
if (newName === workspace.workingBranch) {
|
|
1474
1539
|
return c.json(workspace); // no-op
|
|
1475
1540
|
}
|
|
1476
|
-
const oldWorktreePath =
|
|
1477
|
-
|
|
1541
|
+
const oldWorktreePath = workspace.worktreePath;
|
|
1542
|
+
// Sibling rename: keep the same worktrees-root, swap the branch leaf.
|
|
1543
|
+
// Cannot use `path.dirname` directly because branches with slashes
|
|
1544
|
+
// (e.g. `feature/x`) make the dirname end one level too deep.
|
|
1545
|
+
const oldSuffix = `/${workspace.workingBranch}`;
|
|
1546
|
+
const worktreesRoot = oldWorktreePath.endsWith(oldSuffix)
|
|
1547
|
+
? oldWorktreePath.slice(0, -oldSuffix.length)
|
|
1548
|
+
: path.join(workspace.projectPath, '.worktrees');
|
|
1549
|
+
const newWorktreePath = path.join(worktreesRoot, newName);
|
|
1478
1550
|
// Reject early if the target name is already in use — either as a local
|
|
1479
1551
|
// branch or on origin. Avoids git's generic "already exists" error and
|
|
1480
1552
|
// protects against the same silent-fallback trap the create flow has.
|
|
@@ -1494,9 +1566,11 @@ app.post('/:id/rename-branch', async (c) => {
|
|
|
1494
1566
|
// not the dir, for git operations.
|
|
1495
1567
|
try {
|
|
1496
1568
|
gitOps.moveWorktree(workspace.projectPath, oldWorktreePath, newWorktreePath);
|
|
1569
|
+
workspaceService.updateWorktreePath(id, newWorktreePath);
|
|
1497
1570
|
}
|
|
1498
1571
|
catch (err) {
|
|
1499
1572
|
console.error('[workspaces] Failed to move worktree dir (branch renamed anyway):', err);
|
|
1573
|
+
// worktree_path stays at oldWorktreePath, which still exists on disk
|
|
1500
1574
|
}
|
|
1501
1575
|
const updated = workspaceService.updateWorkingBranch(id, newName);
|
|
1502
1576
|
return c.json(updated);
|
|
@@ -1517,7 +1591,12 @@ app.post('/:id/resync-branch', (c) => {
|
|
|
1517
1591
|
if (!workspace) {
|
|
1518
1592
|
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
1519
1593
|
}
|
|
1520
|
-
|
|
1594
|
+
if (!workspace.worktreeOwned) {
|
|
1595
|
+
return c.json({
|
|
1596
|
+
error: 'Resync-branch is not available for attached external worktrees.',
|
|
1597
|
+
}, 400);
|
|
1598
|
+
}
|
|
1599
|
+
const worktreePath = workspace.worktreePath;
|
|
1521
1600
|
let actual;
|
|
1522
1601
|
try {
|
|
1523
1602
|
actual = gitOps.getCurrentBranch(worktreePath).trim();
|
|
@@ -1531,18 +1610,24 @@ app.post('/:id/resync-branch', (c) => {
|
|
|
1531
1610
|
return c.json({ ok: true, changed: false, workingBranch: workspace.workingBranch });
|
|
1532
1611
|
}
|
|
1533
1612
|
// Branch was renamed in-place by the agent (`git branch -m ...`). The
|
|
1534
|
-
// worktree directory is still at
|
|
1535
|
-
// matches the new ref, otherwise Kōbō's path resolver
|
|
1536
|
-
//
|
|
1537
|
-
//
|
|
1538
|
-
//
|
|
1539
|
-
//
|
|
1540
|
-
const
|
|
1613
|
+
// worktree directory is still at <worktrees-root>/<old-name>; move it so it
|
|
1614
|
+
// matches the new ref, otherwise Kōbō's path resolver breaks and
|
|
1615
|
+
// subsequent session spawns fail with ENOENT on .mcp.json. Best-effort:
|
|
1616
|
+
// if the move fails (dir already moved, lockfile, dirty tree), we still
|
|
1617
|
+
// update the DB so git ops stay aligned with the current ref name — the
|
|
1618
|
+
// user can repair the dir manually.
|
|
1619
|
+
const oldSuffix = `/${workspace.workingBranch}`;
|
|
1620
|
+
const worktreesRoot = worktreePath.endsWith(oldSuffix)
|
|
1621
|
+
? worktreePath.slice(0, -oldSuffix.length)
|
|
1622
|
+
: path.join(workspace.projectPath, '.worktrees');
|
|
1623
|
+
const newWorktreePath = path.join(worktreesRoot, actual);
|
|
1541
1624
|
try {
|
|
1542
1625
|
gitOps.moveWorktree(workspace.projectPath, worktreePath, newWorktreePath);
|
|
1626
|
+
workspaceService.updateWorktreePath(id, newWorktreePath);
|
|
1543
1627
|
}
|
|
1544
1628
|
catch (err) {
|
|
1545
1629
|
console.error('[workspaces] resync-branch: moveWorktree failed (DB update proceeds):', err);
|
|
1630
|
+
// worktree_path stays at the old path; DB update for working branch still proceeds
|
|
1546
1631
|
}
|
|
1547
1632
|
const updated = workspaceService.updateWorkingBranch(id, actual);
|
|
1548
1633
|
return c.json({ ok: true, changed: true, workingBranch: updated.workingBranch });
|
|
@@ -1562,7 +1647,7 @@ app.post('/:id/push', async (c) => {
|
|
|
1562
1647
|
}
|
|
1563
1648
|
const body = await c.req.json().catch(() => ({}));
|
|
1564
1649
|
const force = body?.force === true;
|
|
1565
|
-
const worktreePath =
|
|
1650
|
+
const worktreePath = workspace.worktreePath;
|
|
1566
1651
|
try {
|
|
1567
1652
|
// Only pass an options arg when force is requested — keeps the
|
|
1568
1653
|
// no-options call shape identical to before for callers/tests that
|
|
@@ -1596,7 +1681,7 @@ app.post('/:id/pull', (c) => {
|
|
|
1596
1681
|
if (!workspace) {
|
|
1597
1682
|
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
1598
1683
|
}
|
|
1599
|
-
const worktreePath =
|
|
1684
|
+
const worktreePath = workspace.worktreePath;
|
|
1600
1685
|
try {
|
|
1601
1686
|
gitOps.pullBranch(worktreePath, workspace.workingBranch);
|
|
1602
1687
|
}
|
|
@@ -1621,7 +1706,7 @@ app.post('/:id/rebase', (c) => {
|
|
|
1621
1706
|
const workspace = workspaceService.getWorkspace(id);
|
|
1622
1707
|
if (!workspace)
|
|
1623
1708
|
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
1624
|
-
const worktreePath =
|
|
1709
|
+
const worktreePath = workspace.worktreePath;
|
|
1625
1710
|
gitOps.rebaseBranch(worktreePath, workspace.sourceBranch);
|
|
1626
1711
|
return c.json({ success: true });
|
|
1627
1712
|
}
|
|
@@ -1640,7 +1725,7 @@ app.post('/:id/merge', (c) => {
|
|
|
1640
1725
|
const workspace = workspaceService.getWorkspace(id);
|
|
1641
1726
|
if (!workspace)
|
|
1642
1727
|
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
1643
|
-
const worktreePath =
|
|
1728
|
+
const worktreePath = workspace.worktreePath;
|
|
1644
1729
|
gitOps.mergeBranch(worktreePath, workspace.sourceBranch);
|
|
1645
1730
|
return c.json({ success: true });
|
|
1646
1731
|
}
|
|
@@ -1659,7 +1744,7 @@ app.post('/:id/git/abort', (c) => {
|
|
|
1659
1744
|
const workspace = workspaceService.getWorkspace(id);
|
|
1660
1745
|
if (!workspace)
|
|
1661
1746
|
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
1662
|
-
const worktreePath =
|
|
1747
|
+
const worktreePath = workspace.worktreePath;
|
|
1663
1748
|
const aborted = gitOps.abortOngoingGitOperation(worktreePath);
|
|
1664
1749
|
return c.json({ success: true, aborted });
|
|
1665
1750
|
}
|
|
@@ -1676,7 +1761,7 @@ app.post('/:id/git/resolve-with-agent', migrationGuard, async (c) => {
|
|
|
1676
1761
|
if (!workspace)
|
|
1677
1762
|
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
1678
1763
|
const body = (await c.req.json().catch(() => ({})));
|
|
1679
|
-
const worktreePath =
|
|
1764
|
+
const worktreePath = workspace.worktreePath;
|
|
1680
1765
|
const operation = body.operation ?? gitOps.getOngoingGitOperation(worktreePath) ?? 'merge';
|
|
1681
1766
|
const files = body.files && body.files.length > 0 ? body.files : gitOps.getConflictedFiles(worktreePath);
|
|
1682
1767
|
if (files.length === 0) {
|
|
@@ -1747,7 +1832,7 @@ app.post('/:id/change-pr-base', async (c) => {
|
|
|
1747
1832
|
const workspace = workspaceService.getWorkspace(id);
|
|
1748
1833
|
if (!workspace)
|
|
1749
1834
|
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
1750
|
-
const worktreePath =
|
|
1835
|
+
const worktreePath = workspace.worktreePath;
|
|
1751
1836
|
await execFileAsync('gh', ['pr', 'edit', '--base', body.base], { cwd: worktreePath });
|
|
1752
1837
|
return c.json({ success: true });
|
|
1753
1838
|
}
|
|
@@ -1764,7 +1849,7 @@ app.post('/:id/open-pr', async (c) => {
|
|
|
1764
1849
|
if (!workspace) {
|
|
1765
1850
|
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
1766
1851
|
}
|
|
1767
|
-
const worktreePath =
|
|
1852
|
+
const worktreePath = workspace.worktreePath;
|
|
1768
1853
|
// Verify branch exists on remote
|
|
1769
1854
|
let lsRemoteOut = '';
|
|
1770
1855
|
try {
|
|
@@ -1859,7 +1944,7 @@ app.post('/:id/open-pr', async (c) => {
|
|
|
1859
1944
|
catch {
|
|
1860
1945
|
// Agent not running — resume it with the PR prompt
|
|
1861
1946
|
try {
|
|
1862
|
-
const worktreePathForResume =
|
|
1947
|
+
const worktreePathForResume = workspace.worktreePath;
|
|
1863
1948
|
agentManager.startAgent(workspace.id, worktreePathForResume, rendered, workspace.model, true, workspace.permissionMode, undefined, workspace.reasoningEffort);
|
|
1864
1949
|
workspaceService.updateWorkspaceStatus(workspace.id, 'executing');
|
|
1865
1950
|
messageSent = true;
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
const KOBO_MCP_BRIEF = [
|
|
7
7
|
'[Kōbō MCP] This workspace exposes a dedicated MCP server with tools prefixed `kobo__`.',
|
|
8
|
+
"Non-interactive mode: this session runs via `claude -p`. Tools requiring a synchronous human reply (e.g. `AskUserQuestion`) won't complete — never call them. If you need user input, end the turn with a plain-text question; the user replies asynchronously via the chat UI.",
|
|
9
|
+
'Plan mode: when running with `--permission-mode plan`, the read-only restriction applies to MCP tools too, not just built-ins. For Kōbō: `kobo__list_*`, `kobo__read_document`, `kobo__search_codebase`, `kobo__get_*` are fine; `kobo__mark_task_done`, `kobo__log_thought`, `kobo__set_workspace_status` are mutations and must wait until the plan is approved.',
|
|
8
10
|
'Conventions — read these BEFORE starting work, not as a fallback:',
|
|
9
11
|
'• `kobo__list_tasks` first on any non-trivial turn, then `kobo__mark_task_done` as each item completes.',
|
|
10
12
|
'• `kobo__list_documents` / `kobo__read_document` to discover existing plans and specs under docs/ and .ai/thoughts/ before writing new ones.',
|
|
@@ -704,7 +704,7 @@ function handleQuota(workspaceId, _agentSessionId) {
|
|
|
704
704
|
autoLoopService.onQuotaBackoffExpired(workspaceId);
|
|
705
705
|
}
|
|
706
706
|
else {
|
|
707
|
-
const freshWorkingDir =
|
|
707
|
+
const freshWorkingDir = freshWs.worktreePath;
|
|
708
708
|
startAgent(workspaceId, freshWorkingDir, 'Continue the previous task where you left off.', undefined, true);
|
|
709
709
|
}
|
|
710
710
|
}
|