internaltool-mcp 1.6.9 → 1.6.14
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/index.js +227 -28
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
19
19
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
20
20
|
import { execSync } from 'child_process'
|
|
21
|
-
import { mkdirSync, writeFileSync, unlinkSync, existsSync } from 'fs'
|
|
21
|
+
import { mkdirSync, writeFileSync, unlinkSync, existsSync, readdirSync, statSync } from 'fs'
|
|
22
22
|
import { join } from 'path'
|
|
23
23
|
import { z } from 'zod'
|
|
24
24
|
import { api, login, configure } from './api-client.js'
|
|
@@ -302,10 +302,11 @@ function registerTaskTools(server, { isAdmin, scopedProjectId }) {
|
|
|
302
302
|
|
|
303
303
|
server.tool(
|
|
304
304
|
'park_task',
|
|
305
|
-
`Pause a task and save your current work context.
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
305
|
+
`Pause a task and save your current work context so another developer can safely pick it up.
|
|
306
|
+
|
|
307
|
+
SAFETY REQUIREMENT: All local changes must be committed and pushed before parking.
|
|
308
|
+
If there are uncommitted or unpushed changes, this tool will block and tell you to push first.
|
|
309
|
+
This ensures no work is lost when someone else picks up the task.
|
|
309
310
|
|
|
310
311
|
Set confirmed=false first to preview, then confirmed=true to actually save.`,
|
|
311
312
|
{
|
|
@@ -314,46 +315,117 @@ Set confirmed=false first to preview, then confirmed=true to actually save.`,
|
|
|
314
315
|
remaining: z.string().optional().describe('Specific next steps to complete this task'),
|
|
315
316
|
blockers: z.string().optional().describe('Anything blocking progress'),
|
|
316
317
|
confirmed: z.boolean().optional().default(false).describe('Set true to execute after reviewing the preview'),
|
|
318
|
+
repoPath: z.string().optional().describe('Absolute path to the local git repo (defaults to MCP process working directory).'),
|
|
317
319
|
},
|
|
318
|
-
async ({ taskId, summary = '', remaining = '', blockers = '', confirmed = false }) => {
|
|
320
|
+
async ({ taskId, summary = '', remaining = '', blockers = '', confirmed = false, repoPath }) => {
|
|
321
|
+
const taskRes = await api.get(`/api/tasks/${taskId}`)
|
|
322
|
+
const task = taskRes?.data?.task
|
|
323
|
+
|
|
324
|
+
// ── Safety check: block if there are uncommitted or unpushed changes ──
|
|
325
|
+
const cwd = repoPath || process.cwd()
|
|
326
|
+
const repoRoot = findRepoRoot(cwd)
|
|
327
|
+
let gitBlockers = []
|
|
328
|
+
if (repoRoot) {
|
|
329
|
+
try {
|
|
330
|
+
const porcelain = runGit('status --porcelain=v1', repoRoot)
|
|
331
|
+
const { localState } = parseGitStatus(porcelain)
|
|
332
|
+
if (localState === 'modified') {
|
|
333
|
+
gitBlockers.push('You have uncommitted local changes. Commit them first so your work is not lost:\n git add . && git commit -m "<your message>" && git push')
|
|
334
|
+
}
|
|
335
|
+
} catch { /* non-fatal */ }
|
|
336
|
+
try {
|
|
337
|
+
const currentBranch = runGit('branch --show-current', repoRoot).trim()
|
|
338
|
+
if (currentBranch) {
|
|
339
|
+
runGit('fetch origin', repoRoot)
|
|
340
|
+
const unpushed = parseInt(runGit(`rev-list origin/${currentBranch}..HEAD --count`, repoRoot).trim(), 10) || 0
|
|
341
|
+
if (unpushed > 0) {
|
|
342
|
+
gitBlockers.push(`You have ${unpushed} unpushed commit(s) on "${currentBranch}". Push first so another developer can access them:\n git push origin ${currentBranch}`)
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
} catch { /* remote branch may not exist — treat as unpushed */ }
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (gitBlockers.length > 0) {
|
|
349
|
+
return text({
|
|
350
|
+
blocked: true,
|
|
351
|
+
reason: 'Cannot park task — changes are not fully committed and pushed.',
|
|
352
|
+
gitBlockers,
|
|
353
|
+
message: 'Fix the issues above, then call park_task again.',
|
|
354
|
+
})
|
|
355
|
+
}
|
|
356
|
+
|
|
319
357
|
if (!confirmed) {
|
|
320
358
|
return text({
|
|
321
359
|
preview: {
|
|
322
360
|
action: 'park_task',
|
|
323
361
|
taskId,
|
|
362
|
+
task: task ? { key: task.key, title: task.title, branch: task.github?.headBranch || null } : null,
|
|
324
363
|
willSave: { summary: summary || '(empty)', remaining: remaining || '(empty)', blockers: blockers || '(empty)' },
|
|
364
|
+
cursorRulesNote: task?.cursorRules?.trim() ? `Cursor rules file (.cursor/rules/${task.key?.toLowerCase()}.mdc) will be removed while parked.` : null,
|
|
365
|
+
handoffNote: task?.github?.headBranch
|
|
366
|
+
? `All work is pushed. Another developer can pick this up by running: git fetch origin && git checkout ${task.github.headBranch}`
|
|
367
|
+
: null,
|
|
325
368
|
},
|
|
326
369
|
requiresConfirmation: true,
|
|
327
|
-
message: 'Review the park note above
|
|
370
|
+
message: 'All changes are pushed. Review the park note above then call park_task again with confirmed=true.',
|
|
328
371
|
})
|
|
329
372
|
}
|
|
330
|
-
|
|
373
|
+
|
|
374
|
+
const res = await api.patch(`/api/tasks/${taskId}/park`, { summary, remaining, blockers })
|
|
375
|
+
// Remove cursor rules file while task is parked — another task may be active
|
|
376
|
+
let cursorRulesCleared = null
|
|
377
|
+
if (task?.cursorRules?.trim()) {
|
|
378
|
+
cursorRulesCleared = deleteCursorRulesFile(task.key, repoPath)
|
|
379
|
+
}
|
|
380
|
+
return text({
|
|
381
|
+
...(res?.data || {}),
|
|
382
|
+
cursorRulesCleared,
|
|
383
|
+
handoff: task?.github?.headBranch
|
|
384
|
+
? { branch: task.github.headBranch, checkoutSteps: ['git fetch origin', `git checkout ${task.github.headBranch}`] }
|
|
385
|
+
: null,
|
|
386
|
+
})
|
|
331
387
|
}
|
|
332
388
|
)
|
|
333
389
|
|
|
334
390
|
server.tool(
|
|
335
391
|
'unpark_task',
|
|
336
|
-
'Resume a parked task
|
|
392
|
+
'Resume a parked task — shows exactly what the previous developer left behind and what to do next.',
|
|
337
393
|
{
|
|
338
394
|
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
339
395
|
confirmed: z.boolean().optional().default(false).describe('Set true to execute after reviewing the preview'),
|
|
396
|
+
repoPath: z.string().optional().describe('Absolute path to the local git repo (defaults to MCP process working directory). Used to restore cursor rules file.'),
|
|
340
397
|
},
|
|
341
|
-
async ({ taskId, confirmed = false }) => {
|
|
398
|
+
async ({ taskId, confirmed = false, repoPath }) => {
|
|
399
|
+
const taskRes = await api.get(`/api/tasks/${taskId}`)
|
|
400
|
+
const task = taskRes?.data?.task
|
|
401
|
+
const branch = task?.github?.headBranch || null
|
|
342
402
|
if (!confirmed) {
|
|
343
|
-
const taskRes = await api.get(`/api/tasks/${taskId}`)
|
|
344
|
-
const task = taskRes?.data?.task
|
|
345
403
|
return text({
|
|
346
404
|
preview: {
|
|
347
405
|
action: 'unpark_task',
|
|
348
406
|
taskId,
|
|
349
|
-
task: task ? { key: task.key, title: task.title,
|
|
350
|
-
|
|
407
|
+
task: task ? { key: task.key, title: task.title, column: task.column } : null,
|
|
408
|
+
parkNote: task?.parkNote || null,
|
|
409
|
+
branch,
|
|
410
|
+
handoffSteps: branch ? [
|
|
411
|
+
'git fetch origin',
|
|
412
|
+
`git checkout ${branch}`,
|
|
413
|
+
'git pull origin ' + branch + ' # get any commits the previous developer pushed',
|
|
414
|
+
] : ['No branch linked yet — create one with create_branch'],
|
|
415
|
+
cursorRulesNote: task?.cursorRules?.trim() ? `Cursor rules will be restored to .cursor/rules/${task.key?.toLowerCase()}.mdc` : null,
|
|
416
|
+
willDo: 'Clear park note and mark task as active again',
|
|
351
417
|
},
|
|
352
418
|
requiresConfirmation: true,
|
|
353
|
-
message: '
|
|
419
|
+
message: 'Read the park note and handoff steps above, then call unpark_task again with confirmed=true to take ownership.',
|
|
354
420
|
})
|
|
355
421
|
}
|
|
356
|
-
|
|
422
|
+
const res = await api.patch(`/api/tasks/${taskId}/unpark`, {})
|
|
423
|
+
// Restore cursor rules file now that this task is active again
|
|
424
|
+
let cursorRulesFile = null
|
|
425
|
+
if (task?.cursorRules?.trim()) {
|
|
426
|
+
cursorRulesFile = writeCursorRulesFile(task.key, task.cursorRules, repoPath)
|
|
427
|
+
}
|
|
428
|
+
return text({ ...(res?.data || {}), cursorRulesFile })
|
|
357
429
|
}
|
|
358
430
|
)
|
|
359
431
|
}
|
|
@@ -848,8 +920,9 @@ Use this when a developer says "start task", "brief me on", or "what do I need t
|
|
|
848
920
|
{
|
|
849
921
|
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
850
922
|
confirmed: z.boolean().optional().default(false).describe('Set true after reading the plan to move the task to in_progress'),
|
|
923
|
+
repoPath: z.string().optional().describe('Absolute path to the local git repo (defaults to MCP process working directory). Used to write cursor rules file.'),
|
|
851
924
|
},
|
|
852
|
-
async ({ taskId, confirmed = false }) => {
|
|
925
|
+
async ({ taskId, confirmed = false, repoPath }) => {
|
|
853
926
|
const taskRes = await api.get(`/api/tasks/${taskId}`)
|
|
854
927
|
if (!taskRes?.success) return errorText('Task not found')
|
|
855
928
|
const task = taskRes.data.task
|
|
@@ -903,6 +976,39 @@ Use this when a developer says "start task", "brief me on", or "what do I need t
|
|
|
903
976
|
]
|
|
904
977
|
}
|
|
905
978
|
|
|
979
|
+
// ── Simultaneous work lock ──────────────────────────────────────────────────
|
|
980
|
+
// If the task is already in_progress with a branch, warn before proceeding.
|
|
981
|
+
// This prevents two developers from unknowingly working on the same task.
|
|
982
|
+
const alreadyActive = task.column === 'in_progress' && !!task.github?.headBranch
|
|
983
|
+
const activeAssignees = (task.assignees || []).map(a => a.name || a.email || a.toString())
|
|
984
|
+
|
|
985
|
+
if (!confirmed && alreadyActive) {
|
|
986
|
+
return text({
|
|
987
|
+
warning: {
|
|
988
|
+
type: 'simultaneous_work_detected',
|
|
989
|
+
message: `⚠️ This task is already in progress on branch "${task.github.headBranch}".`,
|
|
990
|
+
assignees: activeAssignees.length ? activeAssignees : ['(unassigned)'],
|
|
991
|
+
branch: task.github.headBranch,
|
|
992
|
+
parkNote: task.parkNote || null,
|
|
993
|
+
advice: activeAssignees.length
|
|
994
|
+
? `Check with ${activeAssignees.join(', ')} before taking over. If they have parked this task their changes are pushed — checkout their branch instead of starting fresh.`
|
|
995
|
+
: `Someone may have been working on this. Check the branch before starting fresh.`,
|
|
996
|
+
checkoutSteps: [
|
|
997
|
+
'git fetch origin',
|
|
998
|
+
`git checkout ${task.github.headBranch}`,
|
|
999
|
+
`git pull origin ${task.github.headBranch}`,
|
|
1000
|
+
],
|
|
1001
|
+
},
|
|
1002
|
+
brief: {
|
|
1003
|
+
key: task.key,
|
|
1004
|
+
title: task.title,
|
|
1005
|
+
priority: task.priority,
|
|
1006
|
+
},
|
|
1007
|
+
requiresConfirmation: true,
|
|
1008
|
+
message: `Task is already in progress. Read the warning above. If you still want to take over, call kickoff_task again with confirmed=true.`,
|
|
1009
|
+
})
|
|
1010
|
+
}
|
|
1011
|
+
|
|
906
1012
|
if (!confirmed) {
|
|
907
1013
|
return text({
|
|
908
1014
|
CURSOR_RULES: hasCursorRules
|
|
@@ -963,7 +1069,7 @@ Use this when a developer says "start task", "brief me on", or "what do I need t
|
|
|
963
1069
|
// Write cursor rules file to local repo immediately on kickoff
|
|
964
1070
|
let cursorRulesFile = null
|
|
965
1071
|
if (hasCursorRules) {
|
|
966
|
-
cursorRulesFile = writeCursorRulesFile(task.key, task.cursorRules)
|
|
1072
|
+
cursorRulesFile = writeCursorRulesFile(task.key, task.cursorRules, repoPath)
|
|
967
1073
|
}
|
|
968
1074
|
|
|
969
1075
|
return text({
|
|
@@ -1238,10 +1344,33 @@ function runGit(args, cwd) {
|
|
|
1238
1344
|
}).trim()
|
|
1239
1345
|
}
|
|
1240
1346
|
|
|
1347
|
+
/**
|
|
1348
|
+
* Find the root of the git repo starting from `startPath`.
|
|
1349
|
+
* If `startPath` itself is not a git repo, scan one level of subdirectories.
|
|
1350
|
+
* Returns the repo root string or null.
|
|
1351
|
+
*/
|
|
1352
|
+
function findRepoRoot(startPath) {
|
|
1353
|
+
const base = startPath || process.cwd()
|
|
1354
|
+
try {
|
|
1355
|
+
return runGit('rev-parse --show-toplevel', base)
|
|
1356
|
+
} catch { /* not a git repo — try subdirectories */ }
|
|
1357
|
+
try {
|
|
1358
|
+
const entries = readdirSync(base, { withFileTypes: true })
|
|
1359
|
+
for (const entry of entries) {
|
|
1360
|
+
if (!entry.isDirectory()) continue
|
|
1361
|
+
try {
|
|
1362
|
+
return runGit('rev-parse --show-toplevel', join(base, entry.name))
|
|
1363
|
+
} catch { /* not a git repo */ }
|
|
1364
|
+
}
|
|
1365
|
+
} catch { /* can't read dir */ }
|
|
1366
|
+
return null
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1241
1369
|
/** Write task-specific cursor rules to .cursor/rules/<taskKey>.mdc in the local repo root. */
|
|
1242
|
-
function writeCursorRulesFile(taskKey, rulesMarkdown) {
|
|
1370
|
+
function writeCursorRulesFile(taskKey, rulesMarkdown, startPath) {
|
|
1243
1371
|
try {
|
|
1244
|
-
const repoRoot =
|
|
1372
|
+
const repoRoot = findRepoRoot(startPath)
|
|
1373
|
+
if (!repoRoot) return null
|
|
1245
1374
|
const rulesDir = join(repoRoot, '.cursor', 'rules')
|
|
1246
1375
|
mkdirSync(rulesDir, { recursive: true })
|
|
1247
1376
|
const filePath = join(rulesDir, `${taskKey.toLowerCase()}.mdc`)
|
|
@@ -1254,9 +1383,10 @@ function writeCursorRulesFile(taskKey, rulesMarkdown) {
|
|
|
1254
1383
|
}
|
|
1255
1384
|
|
|
1256
1385
|
/** Delete the task-specific cursor rules file when work is complete. */
|
|
1257
|
-
function deleteCursorRulesFile(taskKey) {
|
|
1386
|
+
function deleteCursorRulesFile(taskKey, startPath) {
|
|
1258
1387
|
try {
|
|
1259
|
-
const repoRoot =
|
|
1388
|
+
const repoRoot = findRepoRoot(startPath)
|
|
1389
|
+
if (!repoRoot) return null
|
|
1260
1390
|
const filePath = join(repoRoot, '.cursor', 'rules', `${taskKey.toLowerCase()}.mdc`)
|
|
1261
1391
|
if (existsSync(filePath)) {
|
|
1262
1392
|
unlinkSync(filePath)
|
|
@@ -1283,6 +1413,32 @@ function parseGitStatus(porcelain) {
|
|
|
1283
1413
|
return { staged, unstaged, untracked, modified, localState }
|
|
1284
1414
|
}
|
|
1285
1415
|
|
|
1416
|
+
/**
|
|
1417
|
+
* Returns how many commits the current branch is behind/ahead of its base branch,
|
|
1418
|
+
* and whether there are uncommitted changes. Fetches from origin first.
|
|
1419
|
+
* Returns null on any git error (e.g. no remote configured).
|
|
1420
|
+
*/
|
|
1421
|
+
function getBranchSyncStatus(cwd, baseBranch = 'main') {
|
|
1422
|
+
try {
|
|
1423
|
+
runGit('fetch origin', cwd)
|
|
1424
|
+
} catch { /* no remote or no network — continue with local data */ }
|
|
1425
|
+
try {
|
|
1426
|
+
const behind = parseInt(runGit(`rev-list HEAD..origin/${baseBranch} --count`, cwd).trim(), 10) || 0
|
|
1427
|
+
const unpushed = parseInt(runGit(`rev-list origin/${baseBranch}..HEAD --count`, cwd).trim(), 10) || 0
|
|
1428
|
+
// Check if the remote tracking branch for the feature branch exists and has unpushed commits
|
|
1429
|
+
let unpushedOnBranch = 0
|
|
1430
|
+
try {
|
|
1431
|
+
const currentBranch = runGit('branch --show-current', cwd).trim()
|
|
1432
|
+
unpushedOnBranch = parseInt(runGit(`rev-list origin/${currentBranch}..HEAD --count`, cwd).trim(), 10) || 0
|
|
1433
|
+
} catch { /* remote branch may not exist yet — all commits are unpushed */ }
|
|
1434
|
+
const porcelain = runGit('status --porcelain=v1', cwd)
|
|
1435
|
+
const { localState } = parseGitStatus(porcelain)
|
|
1436
|
+
return { behind, unpushed, unpushedOnBranch, localState, baseBranch }
|
|
1437
|
+
} catch {
|
|
1438
|
+
return null
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1286
1442
|
// ── Git Workflow Tools ─────────────────────────────────────────────────────────
|
|
1287
1443
|
// All write operations require confirmed=true after showing a preview.
|
|
1288
1444
|
// This gives developers full visibility before anything is executed.
|
|
@@ -1690,7 +1846,7 @@ If you have uncommitted tracked changes, it will tell you exactly what to do bef
|
|
|
1690
1846
|
const freshTaskForRules = await api.get(`/api/tasks/${taskId}`).catch(() => null)
|
|
1691
1847
|
const cursorRulesContent = freshTaskForRules?.data?.task?.cursorRules
|
|
1692
1848
|
if (cursorRulesContent?.trim()) {
|
|
1693
|
-
cursorRulesFile = writeCursorRulesFile(freshTaskForRules.data.task.key, cursorRulesContent)
|
|
1849
|
+
cursorRulesFile = writeCursorRulesFile(freshTaskForRules.data.task.key, cursorRulesContent, cwd)
|
|
1694
1850
|
}
|
|
1695
1851
|
|
|
1696
1852
|
const checkoutSteps = [
|
|
@@ -1977,8 +2133,9 @@ Set confirmed=false first to preview the full PR content, then confirmed=true to
|
|
|
1977
2133
|
additionalNotes: z.string().optional().describe('Extra context to add to the PR body'),
|
|
1978
2134
|
draft: z.boolean().optional().default(false).describe('Open as a draft PR (not yet ready for review)'),
|
|
1979
2135
|
confirmed: z.boolean().optional().default(false).describe('Set true to create the PR after reviewing the preview'),
|
|
2136
|
+
repoPath: z.string().optional().describe('Absolute path to the local git repo (defaults to MCP process working directory). Used to delete cursor rules file.'),
|
|
1980
2137
|
},
|
|
1981
|
-
async ({ taskId, projectId, headBranch, additionalNotes = '', draft = false, confirmed = false }) => {
|
|
2138
|
+
async ({ taskId, projectId, headBranch, additionalNotes = '', draft = false, confirmed = false, repoPath }) => {
|
|
1982
2139
|
if (scopedProjectId && projectId !== scopedProjectId) {
|
|
1983
2140
|
return errorText(`Access denied: session is scoped to project ${scopedProjectId}`)
|
|
1984
2141
|
}
|
|
@@ -2002,8 +2159,33 @@ Set confirmed=false first to preview the full PR content, then confirmed=true to
|
|
|
2002
2159
|
].filter(v => v !== null).join('\n')
|
|
2003
2160
|
|
|
2004
2161
|
if (!confirmed) {
|
|
2162
|
+
// ── Pre-flight: check branch sync status before allowing PR ──
|
|
2163
|
+
const cwd = repoPath || process.cwd()
|
|
2164
|
+
const repoRoot = findRepoRoot(cwd)
|
|
2165
|
+
const sync = repoRoot ? getBranchSyncStatus(repoRoot) : null
|
|
2166
|
+
|
|
2167
|
+
const preflight = { status: 'ok', warnings: [], blockers: [] }
|
|
2168
|
+
if (sync) {
|
|
2169
|
+
if (sync.behind > 0) {
|
|
2170
|
+
preflight.blockers.push(
|
|
2171
|
+
`Branch is ${sync.behind} commit(s) behind origin/${sync.baseBranch}. Rebase before raising PR to avoid merge conflicts: git fetch origin && git rebase origin/${sync.baseBranch}`
|
|
2172
|
+
)
|
|
2173
|
+
}
|
|
2174
|
+
if (sync.unpushedOnBranch > 0) {
|
|
2175
|
+
preflight.blockers.push(
|
|
2176
|
+
`You have ${sync.unpushedOnBranch} local commit(s) not pushed to remote. Push first: git push origin ${headBranch}`
|
|
2177
|
+
)
|
|
2178
|
+
}
|
|
2179
|
+
if (sync.localState === 'modified') {
|
|
2180
|
+
preflight.warnings.push('You have uncommitted local changes. Commit or stash them before the PR is merged.')
|
|
2181
|
+
}
|
|
2182
|
+
if (preflight.blockers.length > 0) preflight.status = 'blocked'
|
|
2183
|
+
else if (preflight.warnings.length > 0) preflight.status = 'warning'
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2005
2186
|
return text({
|
|
2006
|
-
|
|
2187
|
+
preflight,
|
|
2188
|
+
preview: preflight.status === 'blocked' ? null : {
|
|
2007
2189
|
action: 'raise_pr',
|
|
2008
2190
|
prTitle,
|
|
2009
2191
|
prBody: bodyParts,
|
|
@@ -2011,8 +2193,10 @@ Set confirmed=false first to preview the full PR content, then confirmed=true to
|
|
|
2011
2193
|
draft,
|
|
2012
2194
|
task: { key: task.key, title: task.title },
|
|
2013
2195
|
},
|
|
2014
|
-
requiresConfirmation:
|
|
2015
|
-
message:
|
|
2196
|
+
requiresConfirmation: preflight.status !== 'blocked',
|
|
2197
|
+
message: preflight.status === 'blocked'
|
|
2198
|
+
? `⛔ Cannot raise PR — fix the blockers above first.`
|
|
2199
|
+
: `Will open a${draft ? ' draft' : ''} PR titled "${prTitle}". Call raise_pr again with confirmed=true to create it.`,
|
|
2016
2200
|
})
|
|
2017
2201
|
}
|
|
2018
2202
|
|
|
@@ -2026,7 +2210,7 @@ Set confirmed=false first to preview the full PR content, then confirmed=true to
|
|
|
2026
2210
|
if (!res?.success) return errorText(res?.message || 'Could not create PR')
|
|
2027
2211
|
|
|
2028
2212
|
// Delete the task-specific cursor rules file — coding is done
|
|
2029
|
-
const deletedRulesFile = deleteCursorRulesFile(task.key)
|
|
2213
|
+
const deletedRulesFile = deleteCursorRulesFile(task.key, repoPath)
|
|
2030
2214
|
|
|
2031
2215
|
return text({
|
|
2032
2216
|
prNumber: res.data.prNumber,
|
|
@@ -2220,6 +2404,20 @@ branchAction values (only needed when current branch ≠ task branch):
|
|
|
2220
2404
|
|
|
2221
2405
|
// Case 2: on correct branch — standard preview
|
|
2222
2406
|
const pushCmd = `git push origin ${currentBranch}`
|
|
2407
|
+
|
|
2408
|
+
// Check if branch is behind base — warn developer to rebase before more commits pile up
|
|
2409
|
+
let divergenceWarning = null
|
|
2410
|
+
try {
|
|
2411
|
+
runGit('fetch origin', cwd)
|
|
2412
|
+
const repoRoot = findRepoRoot(cwd)
|
|
2413
|
+
if (repoRoot) {
|
|
2414
|
+
const behind = parseInt(runGit(`rev-list HEAD..origin/main --count`, repoRoot).trim(), 10) || 0
|
|
2415
|
+
if (behind > 0) {
|
|
2416
|
+
divergenceWarning = `⚠️ Your branch is ${behind} commit(s) behind origin/main. Rebase now to prevent merge conflicts later:\n git fetch origin && git rebase origin/main\nThen re-run your commits.`
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
} catch { /* non-fatal */ }
|
|
2420
|
+
|
|
2223
2421
|
return text({
|
|
2224
2422
|
preview: {
|
|
2225
2423
|
suggestedMessage: commitMsg,
|
|
@@ -2230,6 +2428,7 @@ branchAction values (only needed when current branch ≠ task branch):
|
|
|
2230
2428
|
onCorrectBranch: !branchMismatch,
|
|
2231
2429
|
changedFiles: changedFilesList,
|
|
2232
2430
|
},
|
|
2431
|
+
divergenceWarning,
|
|
2233
2432
|
unsafeUntrackedWarning: unsafeUntracked.length
|
|
2234
2433
|
? `These paths should NOT be committed — add them to .gitignore first: ${unsafeUntracked.join(', ')}`
|
|
2235
2434
|
: null,
|
package/package.json
CHANGED