internaltool-mcp 1.6.45 → 1.6.52
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 +591 -137
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -963,7 +963,7 @@ WHAT THIS DOES AUTOMATICALLY (confirmed=true):
|
|
|
963
963
|
1. Verifies all changes are committed and pushed — blocks if not
|
|
964
964
|
2. Auto-pushes any unpushed commits to remote
|
|
965
965
|
3. Saves your park note (summary, remaining, blockers) to the task
|
|
966
|
-
4. Deletes
|
|
966
|
+
4. Deletes all workspace files (.cursor/rules/, .cursor/skills/, .cursor/agents/) so they don't bleed into other tasks
|
|
967
967
|
5. Posts a comment on the task with your handoff notes (visible to Dev B)
|
|
968
968
|
6. Notifies all project members that this task is parked and ready for pickup
|
|
969
969
|
|
|
@@ -1023,8 +1023,10 @@ Set confirmed=false first to preview, then confirmed=true to execute everything.
|
|
|
1023
1023
|
// ── Auto-commit if there are uncommitted changes ───────────────────────
|
|
1024
1024
|
if (repoRoot && uncommitted) {
|
|
1025
1025
|
try {
|
|
1026
|
-
const
|
|
1027
|
-
|
|
1026
|
+
const rawMsg = `wip(${task?.key?.toLowerCase() || 'task'}): ${(summary || 'parking task').slice(0, 60)}`
|
|
1027
|
+
// Sanitize to prevent shell injection — strip/replace special chars
|
|
1028
|
+
const commitMsg = rawMsg.replace(/"/g, "'").replace(/[`$\\]/g, '')
|
|
1029
|
+
runGit('add -u', repoRoot) // only tracked files — avoids staging .env / build artifacts
|
|
1028
1030
|
runGit(`commit -m "${commitMsg}"`, repoRoot)
|
|
1029
1031
|
unpushedCount += 1 // just committed, so now there's something to push
|
|
1030
1032
|
} catch (e) {
|
|
@@ -1087,10 +1089,12 @@ WHAT THIS DOES AUTOMATICALLY (confirmed=true):
|
|
|
1087
1089
|
2. Shows the last 5 commits on the branch so you know the exact state of the code
|
|
1088
1090
|
3. Shows recent task comments so you have full context
|
|
1089
1091
|
4. Runs: git fetch origin + git checkout <branch> + git pull (switches your local repo)
|
|
1090
|
-
5. Restores the cursor
|
|
1091
|
-
6.
|
|
1092
|
-
7.
|
|
1092
|
+
5. Restores the FULL cursor workspace: rules, skills, session-protocol, active-agent.md
|
|
1093
|
+
6. Logs all restored skills/rules to the session activity timeline
|
|
1094
|
+
7. Posts a comment that you picked up the task
|
|
1095
|
+
8. Notifies the previous developer that you have taken over
|
|
1093
1096
|
|
|
1097
|
+
After unparking: READ all injected skills before touching any files. Then re-claim files.
|
|
1094
1098
|
Set confirmed=false first to read everything, then confirmed=true to execute.`,
|
|
1095
1099
|
{
|
|
1096
1100
|
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
@@ -1140,6 +1144,8 @@ Set confirmed=false first to read everything, then confirmed=true to execute.`,
|
|
|
1140
1144
|
`git checkout ${branch}`,
|
|
1141
1145
|
`git pull origin ${branch}`,
|
|
1142
1146
|
task?.cursorRules?.trim() ? `Restore .cursor/rules/${task.key?.toLowerCase()}.mdc` : null,
|
|
1147
|
+
`Restore full cursor workspace (skills, session-protocol, active-agent.md)`,
|
|
1148
|
+
`Log restored workspace files to session timeline`,
|
|
1143
1149
|
'Post "picked up" comment on task',
|
|
1144
1150
|
'Notify previous developer',
|
|
1145
1151
|
].filter(Boolean) : ['No branch linked — create one with create_branch after unparking'],
|
|
@@ -1229,6 +1235,37 @@ Set confirmed=false first to read everything, then confirmed=true to execute.`,
|
|
|
1229
1235
|
api.patch(`/api/tasks/${taskId}`, { agentRole }).catch(() => {/* non-fatal */})
|
|
1230
1236
|
}
|
|
1231
1237
|
|
|
1238
|
+
// ── Re-write full cursor workspace (skills, session protocol, active-agent) ──
|
|
1239
|
+
// park_task deletes everything; unpark_task must restore the complete workspace.
|
|
1240
|
+
let workspaceResult = null
|
|
1241
|
+
try {
|
|
1242
|
+
let projectAgentConfig = null
|
|
1243
|
+
try {
|
|
1244
|
+
const projRes = await api.get(`/api/projects/${task.project}`)
|
|
1245
|
+
if (projRes?.success) projectAgentConfig = projRes.data.project?.agentConfig || null
|
|
1246
|
+
} catch { /* non-fatal */ }
|
|
1247
|
+
|
|
1248
|
+
const taskForWorkspace = agentRole ? { ...task, agentRole } : task
|
|
1249
|
+
workspaceResult = writeCursorWorkspace(taskForWorkspace, projectAgentConfig, repoPath || process.cwd())
|
|
1250
|
+
|
|
1251
|
+
// Log injected skills/rules to session timeline
|
|
1252
|
+
// Log each written file to session timeline
|
|
1253
|
+
const writtenFiles = workspaceResult?.written || []
|
|
1254
|
+
for (const filePath of writtenFiles) {
|
|
1255
|
+
const fileName = filePath.split('/').pop().replace(/\.(md|mdc)$/, '')
|
|
1256
|
+
const isSkill = filePath.includes('/.cursor/skills/')
|
|
1257
|
+
const isRule = filePath.includes('/.cursor/rules/')
|
|
1258
|
+
const isAgent = filePath.includes('/.cursor/agents/')
|
|
1259
|
+
const type = isSkill ? 'skill' : isRule ? 'rule' : isAgent ? 'subagent' : 'info'
|
|
1260
|
+
api.post(`/api/tasks/${taskId}/session/log`, {
|
|
1261
|
+
type,
|
|
1262
|
+
name: fileName,
|
|
1263
|
+
role: agentRole || null,
|
|
1264
|
+
summary: `📖 Injected at unpark: ${filePath.replace(workspaceResult.repoRoot, '')}`,
|
|
1265
|
+
}).catch(() => {})
|
|
1266
|
+
}
|
|
1267
|
+
} catch { /* non-fatal — workspace restore failure should not block the unpark */ }
|
|
1268
|
+
|
|
1232
1269
|
// ── #9 Last commit metadata for handoff context ───────────────────────
|
|
1233
1270
|
const lastCommit = getLastCommitMeta(repoRoot)
|
|
1234
1271
|
|
|
@@ -1241,21 +1278,30 @@ Set confirmed=false first to read everything, then confirmed=true to execute.`,
|
|
|
1241
1278
|
cursorRules: cursorRulesFile
|
|
1242
1279
|
? { restored: true, path: cursorRulesFile, agentRole: agentRole || null }
|
|
1243
1280
|
: { restored: false },
|
|
1281
|
+
workspace: workspaceResult
|
|
1282
|
+
? { restored: true, filesCount: workspaceResult.written?.length || 0, files: workspaceResult.written || [] }
|
|
1283
|
+
: { restored: false },
|
|
1244
1284
|
commentPosted: true,
|
|
1245
1285
|
previousDevNotified: true,
|
|
1246
1286
|
parkNote: task?.parkNote || null,
|
|
1247
1287
|
message: gitResult?.switched
|
|
1248
|
-
? `You are now on branch "${branch}". Cursor
|
|
1249
|
-
: `Branch switch failed — see git.manualSteps. Cursor
|
|
1288
|
+
? `You are now on branch "${branch}". Cursor workspace restored${agentRole ? ` (role: ${agentRole})` : ''}. Read all skills before editing.`
|
|
1289
|
+
: `Branch switch failed — see git.manualSteps. Cursor workspace restored.`,
|
|
1290
|
+
previouslyClaimed: task?.claimedFiles?.length ? task.claimedFiles : null,
|
|
1250
1291
|
nextStep: agentRole === 'builder'
|
|
1251
|
-
?
|
|
1292
|
+
? [
|
|
1293
|
+
`BUILDER role active.`,
|
|
1294
|
+
`1. ⚠️ READ all injected skills first (listed in workspace.files) — mandatory before any edits`,
|
|
1295
|
+
`2. Re-claim your files: claim_files(taskId="${taskId}", files=${JSON.stringify(task?.claimedFiles || [])})`,
|
|
1296
|
+
`3. Then continue implementation from where the previous developer left off`,
|
|
1297
|
+
].join('\n')
|
|
1252
1298
|
: agentRole === 'scout'
|
|
1253
1299
|
? `SCOUT role active. Read-only mode — map the codebase and save findings with update_task(scoutReport=...).`
|
|
1254
1300
|
: agentRole === 'coordinator'
|
|
1255
1301
|
? `COORDINATOR role active. Call decompose_task to plan parallel workstreams.`
|
|
1256
1302
|
: agentRole === 'reviewer'
|
|
1257
1303
|
? `REVIEWER role active. Call review_pr to start the review chain.`
|
|
1258
|
-
:
|
|
1304
|
+
: `Read all injected skills in workspace.files before making any changes.`,
|
|
1259
1305
|
})
|
|
1260
1306
|
}
|
|
1261
1307
|
)
|
|
@@ -1537,52 +1583,92 @@ Call confirmed=false to preview the decomposition, confirmed=true to save it.`,
|
|
|
1537
1583
|
templates: decompositionTemplates,
|
|
1538
1584
|
} : null,
|
|
1539
1585
|
warnings: [
|
|
1540
|
-
!executionPlan.scoutReportPresent
|
|
1541
|
-
|
|
1586
|
+
!executionPlan.scoutReportPresent
|
|
1587
|
+
? '⛔ SCOUT REPORT MISSING — Strongly recommended before decomposing. Builders without a scout map will guess at file ownership and produce overlapping work. Call kickoff_task(agentRole="scout", confirmed=true) first.'
|
|
1588
|
+
: null,
|
|
1589
|
+
!executionPlan.readmePresent
|
|
1590
|
+
? '⛔ NO IMPLEMENTATION PLAN — Builders cannot implement without a spec. Call update_task(readmeMarkdown=...) before decomposing.'
|
|
1591
|
+
: null,
|
|
1592
|
+
subtaskPlan.some(s => !s.files?.length)
|
|
1593
|
+
? `⚠️ ${subtaskPlan.filter(s => !s.files?.length).map(s => s.title).join(', ')} — no files declared. Every subtask MUST own at least one file to prevent two builders editing the same code.`
|
|
1594
|
+
: null,
|
|
1542
1595
|
].filter(Boolean),
|
|
1596
|
+
scoutFirstInstruction: !executionPlan.scoutReportPresent
|
|
1597
|
+
? `MANDATORY: call scout_task(taskId="${task._id}", confirmed=false) then scout_task(confirmed=true, report="<your findings>") before confirming this decomposition. Do NOT switch agentRole — run scout_task directly as coordinator.`
|
|
1598
|
+
: null,
|
|
1543
1599
|
requiresConfirmation: true,
|
|
1544
1600
|
message: 'Review the decomposition above. Call decompose_task again with confirmed=true to save it and create the subtasks on the board.',
|
|
1545
1601
|
})
|
|
1546
1602
|
}
|
|
1547
1603
|
|
|
1548
|
-
// Save decomposition JSON to parent task
|
|
1549
|
-
const decompositionJson = JSON.stringify(executionPlan, null, 2)
|
|
1550
|
-
try {
|
|
1551
|
-
await api.patch(`/api/tasks/${taskId}`, { decomposition: decompositionJson })
|
|
1552
|
-
} catch { /* non-fatal */ }
|
|
1553
|
-
|
|
1554
1604
|
// Create real child tasks for each subtask in the plan
|
|
1605
|
+
// Do this BEFORE saving the decomposition JSON so we can embed child task IDs.
|
|
1555
1606
|
const projectId = task.project?._id || task.project
|
|
1556
1607
|
const createdTasks = []
|
|
1608
|
+
// Build a map from subtask title → child task info for embedding into executionPlan
|
|
1609
|
+
const childTaskByTitle = {}
|
|
1610
|
+
|
|
1557
1611
|
for (const s of subtaskPlan) {
|
|
1558
1612
|
try {
|
|
1613
|
+
// Role → generic role-appropriate skills that any project should have.
|
|
1614
|
+
// We avoid hardcoding third-party skill names (e.g. zop-*) because projects
|
|
1615
|
+
// may not have those configured — the agent would look for files that don't exist.
|
|
1616
|
+
// The project-level agentConfig.skills drive what actually gets written to .cursor/skills/.
|
|
1617
|
+
const ROLE_KIT = {
|
|
1618
|
+
builder: { skills: { 'run-tests': true, 'commit-conventions': true }, subagents: {}, prompts: {} },
|
|
1619
|
+
scout: { skills: { 'scout-codebase': true }, subagents: {}, prompts: {} },
|
|
1620
|
+
reviewer: { skills: { 'review-checklist': true, 'run-tests': true }, subagents: {}, prompts: {} },
|
|
1621
|
+
coordinator: { skills: { 'scout-codebase': true, 'run-tests': true }, subagents: {}, prompts: {} },
|
|
1622
|
+
}
|
|
1623
|
+
const childKit = ROLE_KIT[s.role] || ROLE_KIT.builder
|
|
1624
|
+
|
|
1559
1625
|
const childRes = await api.post(`/api/projects/${projectId}/tasks`, {
|
|
1560
1626
|
title: `[${s.role.toUpperCase()}] ${s.title}`,
|
|
1561
1627
|
description: s.description,
|
|
1562
1628
|
readmeMarkdown: [
|
|
1563
1629
|
`## Role: ${s.role}`,
|
|
1630
|
+
`## Parent task: ${task.key} — ${task.title}`,
|
|
1564
1631
|
`## Description\n${s.description}`,
|
|
1565
1632
|
s.files?.length ? `## Files to claim at kickoff\n${s.files.map(f => `- \`${f}\``).join('\n')}` : '',
|
|
1566
|
-
s.dependsOn?.length ? `## Depends on\n${s.dependsOn.map(d => `- ${d}`).join('\n')}` : '',
|
|
1633
|
+
s.dependsOn?.length ? `## Depends on (must complete before this starts)\n${s.dependsOn.map(d => `- ${d}`).join('\n')}` : '',
|
|
1634
|
+
`## Skills to read BEFORE editing\nRead every .md file in .cursor/skills/ that was written at kickoff.`,
|
|
1635
|
+
`## After finishing\nCall park_task or raise_pr, then update the parent task (${task.key}) subtask checklist.`,
|
|
1567
1636
|
].filter(Boolean).join('\n\n'),
|
|
1568
|
-
column:
|
|
1569
|
-
priority:
|
|
1570
|
-
taskType:
|
|
1571
|
-
parentTask:
|
|
1637
|
+
column: 'todo',
|
|
1638
|
+
priority: task.priority || 'medium',
|
|
1639
|
+
taskType: s.role === 'reviewer' ? 'feature' : (task.taskType || 'feature'),
|
|
1640
|
+
parentTask: taskId,
|
|
1572
1641
|
suggestedFiles: s.files || [],
|
|
1642
|
+
agentKitOverrides: childKit,
|
|
1573
1643
|
})
|
|
1574
1644
|
if (childRes?.success) {
|
|
1575
|
-
|
|
1645
|
+
const childInfo = {
|
|
1576
1646
|
taskId: childRes.data?.task?._id,
|
|
1577
1647
|
taskKey: childRes.data?.task?.key,
|
|
1578
1648
|
title: childRes.data?.task?.title,
|
|
1579
1649
|
role: s.role,
|
|
1580
1650
|
files: s.files,
|
|
1581
|
-
}
|
|
1651
|
+
}
|
|
1652
|
+
createdTasks.push(childInfo)
|
|
1653
|
+
childTaskByTitle[s.title] = childInfo
|
|
1582
1654
|
}
|
|
1583
1655
|
} catch { /* non-fatal — continue creating remaining tasks */ }
|
|
1584
1656
|
}
|
|
1585
1657
|
|
|
1658
|
+
// Embed child task IDs into the execution plan so get_parallel_kickoffs can route
|
|
1659
|
+
// each builder to their own child task (not the parent task).
|
|
1660
|
+
executionPlan.subtasks = executionPlan.subtasks.map(s => ({
|
|
1661
|
+
...s,
|
|
1662
|
+
childTaskId: childTaskByTitle[s.title]?.taskId || null,
|
|
1663
|
+
childTaskKey: childTaskByTitle[s.title]?.taskKey || null,
|
|
1664
|
+
}))
|
|
1665
|
+
|
|
1666
|
+
// Save decomposition JSON to parent task — now includes child task IDs
|
|
1667
|
+
const decompositionJson = JSON.stringify(executionPlan, null, 2)
|
|
1668
|
+
try {
|
|
1669
|
+
await api.patch(`/api/tasks/${taskId}`, { decomposition: decompositionJson })
|
|
1670
|
+
} catch { /* non-fatal */ }
|
|
1671
|
+
|
|
1586
1672
|
return text({
|
|
1587
1673
|
decomposed: true,
|
|
1588
1674
|
taskKey: task.key,
|
|
@@ -1603,16 +1689,17 @@ Call confirmed=false to preview the decomposition, confirmed=true to save it.`,
|
|
|
1603
1689
|
`Scout agent entry point: analyze the codebase for a task and save a structured scout report.
|
|
1604
1690
|
|
|
1605
1691
|
Call this BEFORE builders start when the Coordinator needs a codebase map.
|
|
1606
|
-
|
|
1692
|
+
Coordinators call this directly — do NOT switch agentRole to "scout" to call it.
|
|
1693
|
+
Agents with agentRole="scout" also call this as their primary work tool.
|
|
1607
1694
|
|
|
1608
1695
|
Two-phase flow:
|
|
1609
1696
|
Phase 1 — confirmed=false: get the briefing (what to analyze, report format)
|
|
1610
1697
|
Phase 2 — confirmed=true + report: save your findings to the task
|
|
1611
1698
|
|
|
1612
1699
|
The scout report is consumed by:
|
|
1613
|
-
- decompose_task (warns if missing)
|
|
1614
|
-
- get_agent_context (included in builder
|
|
1615
|
-
- kickoff_task (surfaced as part of builder brief)
|
|
1700
|
+
- decompose_task (warns if missing, refuses to decompose without one)
|
|
1701
|
+
- get_agent_context (automatically included in builder system prompt)
|
|
1702
|
+
- kickoff_task (surfaced as part of the builder brief)
|
|
1616
1703
|
|
|
1617
1704
|
Scouts MUST NOT modify any source code files or create branches.`,
|
|
1618
1705
|
{
|
|
@@ -1929,11 +2016,24 @@ This tool posts a comment on the task (for visibility in the app) and notifies a
|
|
|
1929
2016
|
trackTaskActivity(taskId, 'request_human_input')
|
|
1930
2017
|
await api.post(`/api/tasks/${taskId}/ask-human`, { question, context, type }).catch(() => null)
|
|
1931
2018
|
|
|
2019
|
+
// Detect whether we're likely running as a Cursor Background Agent (non-interactive).
|
|
2020
|
+
// Background agents cannot receive chat replies — the human must check the InternalTool
|
|
2021
|
+
// task comments to see the question, then the agent should use a safe default and move on.
|
|
2022
|
+
// We surface this explicitly so the agent does not pause indefinitely.
|
|
2023
|
+
const isBackgroundLikely = !process.stdin.isTTY
|
|
2024
|
+
|
|
1932
2025
|
const typeLabel = type === 'approval' ? '🔐 APPROVAL NEEDED' : type === 'ambiguity' ? '🤔 AMBIGUITY — YOUR DECISION' : '❓ QUESTION FOR YOU'
|
|
1933
2026
|
return text({
|
|
1934
2027
|
[typeLabel]: question,
|
|
1935
2028
|
...(context ? { context } : {}),
|
|
1936
|
-
|
|
2029
|
+
backgroundAgentWarning: isBackgroundLikely
|
|
2030
|
+
? 'You appear to be running as a Background Agent (no interactive TTY). The question has been posted as a task comment and the developer has been notified. Since there is no interactive chat channel, DO NOT wait indefinitely. Use the safest autonomous default and proceed, then note your decision in a log_session_event call.'
|
|
2031
|
+
: null,
|
|
2032
|
+
instruction: isBackgroundLikely
|
|
2033
|
+
? 'Background agent mode: proceed with the safest default decision. Log your choice with log_session_event(type="info", name="autonomous-decision", summary="<what you decided and why>").'
|
|
2034
|
+
: 'Please reply in this Cursor chat. I will use your answer to continue. Do NOT proceed until you reply.',
|
|
2035
|
+
taskCommentPosted: true,
|
|
2036
|
+
developerNotified: true,
|
|
1937
2037
|
})
|
|
1938
2038
|
}
|
|
1939
2039
|
)
|
|
@@ -2069,13 +2169,24 @@ Returns systemPrompt ready to use as a Claude system prompt.`,
|
|
|
2069
2169
|
return taskOverride !== undefined ? taskOverride : s.enabled !== false
|
|
2070
2170
|
})
|
|
2071
2171
|
|
|
2072
|
-
// Role-based relevance filter: each role only gets skills that match its job
|
|
2172
|
+
// Role-based relevance filter: each role only gets skills that match its job.
|
|
2173
|
+
// Keywords for each role are matched against skill name + description.
|
|
2073
2174
|
const ROLE_SKILL_KEYWORDS = {
|
|
2074
|
-
builder: ['test', 'run', 'build', 'lint', 'commit', 'style', 'deploy', 'migration', 'seed'
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2175
|
+
builder: ['test', 'run', 'build', 'lint', 'commit', 'style', 'deploy', 'migration', 'seed',
|
|
2176
|
+
'implement', 'feature', 'api', 'endpoint', 'component', 'service', 'hook',
|
|
2177
|
+
'util', 'helper', 'create', 'update', 'delete', 'fix', 'patch', 'convention'],
|
|
2178
|
+
scout: ['scout', 'codebase', 'explore', 'map', 'analyze', 'read', 'report',
|
|
2179
|
+
'find', 'locate', 'search', 'survey', 'inventory', 'document', 'recon'],
|
|
2180
|
+
reviewer: ['review', 'security', 'audit', 'test', 'quality', 'check', 'verify',
|
|
2181
|
+
'validate', 'pr', 'pull', 'lint', 'coverage', 'performance', 'checklist'],
|
|
2182
|
+
coordinator: ['decompose', 'plan', 'coordinate', 'parallel', 'orchestrate', 'manage',
|
|
2183
|
+
'delegate', 'assign', 'breakdown', 'workflow', 'group', 'subtask'],
|
|
2184
|
+
}
|
|
2185
|
+
// Skills that don't match any role-specific keyword are treated as universal
|
|
2186
|
+
// (e.g. 'session-start', 'workflow-guide') and given to all roles.
|
|
2187
|
+
const ALL_ROLE_KEYWORDS = Object.values(ROLE_SKILL_KEYWORDS).flat()
|
|
2188
|
+
const isRoleSpecific = (s) => ALL_ROLE_KEYWORDS.some(kw => `${s.name} ${s.description || ''}`.toLowerCase().includes(kw))
|
|
2189
|
+
|
|
2079
2190
|
const roleKeywords = effectiveRole ? (ROLE_SKILL_KEYWORDS[effectiveRole] || []) : []
|
|
2080
2191
|
|
|
2081
2192
|
// Also use the task type's suggestedSkills list for relevance
|
|
@@ -2088,11 +2199,13 @@ Returns systemPrompt ready to use as a Claude system prompt.`,
|
|
|
2088
2199
|
})()
|
|
2089
2200
|
|
|
2090
2201
|
const relevantSkills = enabledSkills.filter(s => {
|
|
2091
|
-
// Always include explicitly overridden-true skills
|
|
2202
|
+
// Always include explicitly overridden-true skills (task-level config)
|
|
2092
2203
|
if (taskSkillOverrides[s.name] === true) return true
|
|
2093
|
-
//
|
|
2204
|
+
// Always include task-type suggested skills
|
|
2094
2205
|
if (taskTypeSkills.includes(s.name)) return true
|
|
2095
|
-
// Include
|
|
2206
|
+
// Include universal skills (don't match any role's keywords — generic helpers)
|
|
2207
|
+
if (!isRoleSpecific(s)) return true
|
|
2208
|
+
// Include if skill name/description matches this role's keywords
|
|
2096
2209
|
const haystack = `${s.name} ${s.description || ''}`.toLowerCase()
|
|
2097
2210
|
return roleKeywords.some(kw => haystack.includes(kw))
|
|
2098
2211
|
})
|
|
@@ -2260,9 +2373,14 @@ Returns reviewId — save it and pass it to merge_pr to prove semantic review ha
|
|
|
2260
2373
|
if (!prNumber) return errorText('No PR linked to this task. Call raise_pr first.')
|
|
2261
2374
|
|
|
2262
2375
|
// Enforce that analysisPoints are specific — reject obviously generic ones
|
|
2263
|
-
const genericPhrases = [
|
|
2376
|
+
const genericPhrases = [
|
|
2377
|
+
'looks good', 'lgtm', 'code is fine', 'no issues', 'all good', 'seems correct',
|
|
2378
|
+
'looks great', 'looks fine', 'all correct', 'perfect', 'seems fine', 'this is fine',
|
|
2379
|
+
'this is correct', 'no problems', 'nothing wrong', 'good job', 'well done',
|
|
2380
|
+
'approved', 'ship it', 'looks right', 'seems right', 'everything is fine',
|
|
2381
|
+
]
|
|
2264
2382
|
const tooGeneric = analysisPoints.filter(p =>
|
|
2265
|
-
p.trim().length <
|
|
2383
|
+
p.trim().length < 25 || genericPhrases.some(g => p.toLowerCase().includes(g))
|
|
2266
2384
|
)
|
|
2267
2385
|
if (tooGeneric.length > 0) {
|
|
2268
2386
|
return text({
|
|
@@ -2406,8 +2524,15 @@ The task moves to Done automatically via the GitHub webhook.`,
|
|
|
2406
2524
|
{ check: `CI checks (${checks?.total ?? 0} runs)`,
|
|
2407
2525
|
pass: skipChecks ? null : (checks?.allPassed ?? null),
|
|
2408
2526
|
note: skipChecks ? 'Skipped by request' : checks?.anyFailed ? 'Some checks failed' : checks?.allPassed ? 'All passing' : 'No checks found' },
|
|
2409
|
-
|
|
2410
|
-
|
|
2527
|
+
// Hard block: PR must have at least one GitHub approval OR a sessionReview from this session.
|
|
2528
|
+
// A sessionReview alone (reviewer = author) is not enough — someone independent must approve.
|
|
2529
|
+
// Exception: if there's a sessionReview AND existing approvals, both gates are satisfied.
|
|
2530
|
+
{ check: 'Has at least one GitHub approval', pass: pr.approvals > 0,
|
|
2531
|
+
note: pr.approvals === 0
|
|
2532
|
+
? hasSessionReview
|
|
2533
|
+
? '⚠️ Self-review only — no independent GitHub approval. Ask a teammate to approve on GitHub before merging.'
|
|
2534
|
+
: 'No approvals. Call review_pr → post_pr_review, then ask a reviewer to approve on GitHub.'
|
|
2535
|
+
: `${pr.approvals} GitHub approval(s)` },
|
|
2411
2536
|
]
|
|
2412
2537
|
|
|
2413
2538
|
const hardBlocks = safetyChecks.filter(c => c.pass === false && c.check !== `CI checks (${checks?.total ?? 0} runs)`)
|
|
@@ -2456,14 +2581,17 @@ The task moves to Done automatically via the GitHub webhook.`,
|
|
|
2456
2581
|
return text({ merged: true, message: 'PR was already merged.' })
|
|
2457
2582
|
}
|
|
2458
2583
|
|
|
2584
|
+
// Move task to done immediately — don't rely solely on the GitHub webhook
|
|
2585
|
+
try { await api.post(`/api/tasks/${taskId}/move`, { column: 'done', toIndex: 0 }) } catch { /* webhook fallback */ }
|
|
2586
|
+
|
|
2459
2587
|
return text({
|
|
2460
2588
|
merged: true,
|
|
2461
2589
|
sha: mergeRes.data?.sha,
|
|
2462
2590
|
prNumber,
|
|
2463
2591
|
taskKey: task.key,
|
|
2464
2592
|
mergeMethod,
|
|
2465
|
-
message: `✅ PR #${prNumber} merged via ${mergeMethod}. Task "${task.key}"
|
|
2466
|
-
nextStep: `
|
|
2593
|
+
message: `✅ PR #${prNumber} merged via ${mergeMethod}. Task "${task.key}" moved to Done.`,
|
|
2594
|
+
nextStep: `Run deleteCursorWorkspace cleanup if any agent files remain, then pick up your next task.`,
|
|
2467
2595
|
})
|
|
2468
2596
|
}
|
|
2469
2597
|
)
|
|
@@ -2664,6 +2792,34 @@ Flow:
|
|
|
2664
2792
|
api.patch(`/api/tasks/${taskId}`, { agentRole }).catch(() => {/* non-fatal */})
|
|
2665
2793
|
}
|
|
2666
2794
|
|
|
2795
|
+
// Re-write full cursor workspace (skills, session protocol, active-agent)
|
|
2796
|
+
// resume_task is equivalent to unpark_task for workspace purposes.
|
|
2797
|
+
let workspaceResult = null
|
|
2798
|
+
try {
|
|
2799
|
+
let projectAgentConfig = null
|
|
2800
|
+
try {
|
|
2801
|
+
const projRes = await api.get(`/api/projects/${task.project}`)
|
|
2802
|
+
if (projRes?.success) projectAgentConfig = projRes.data.project?.agentConfig || null
|
|
2803
|
+
} catch { /* non-fatal */ }
|
|
2804
|
+
|
|
2805
|
+
const taskForWorkspace = agentRole ? { ...task, agentRole } : task
|
|
2806
|
+
workspaceResult = writeCursorWorkspace(taskForWorkspace, projectAgentConfig, repoPath || process.cwd())
|
|
2807
|
+
|
|
2808
|
+
// Log injected files to session timeline
|
|
2809
|
+
const writtenFiles = workspaceResult?.written || []
|
|
2810
|
+
for (const filePath of writtenFiles) {
|
|
2811
|
+
const fileName = filePath.split('/').pop().replace(/\.(md|mdc)$/, '')
|
|
2812
|
+
const isSkill = filePath.includes('/.cursor/skills/')
|
|
2813
|
+
const isRule = filePath.includes('/.cursor/rules/')
|
|
2814
|
+
const isAgent = filePath.includes('/.cursor/agents/')
|
|
2815
|
+
const type = isSkill ? 'skill' : isRule ? 'rule' : isAgent ? 'subagent' : 'info'
|
|
2816
|
+
api.post(`/api/tasks/${taskId}/session/log`, {
|
|
2817
|
+
type, name: fileName, role: agentRole || null,
|
|
2818
|
+
summary: `📖 Injected at resume: ${filePath.replace(workspaceResult.repoRoot, '')}`,
|
|
2819
|
+
}).catch(() => {})
|
|
2820
|
+
}
|
|
2821
|
+
} catch { /* non-fatal */ }
|
|
2822
|
+
|
|
2667
2823
|
// Last commit metadata
|
|
2668
2824
|
const lastCommit = getLastCommitMeta(repoRoot)
|
|
2669
2825
|
|
|
@@ -2679,11 +2835,20 @@ Flow:
|
|
|
2679
2835
|
cursorRules: cursorRulesFile
|
|
2680
2836
|
? { restored: true, path: cursorRulesFile, agentRole: agentRole || null }
|
|
2681
2837
|
: { restored: false },
|
|
2838
|
+
workspace: workspaceResult
|
|
2839
|
+
? { restored: true, filesCount: workspaceResult.written?.length || 0, files: workspaceResult.written || [] }
|
|
2840
|
+
: { restored: false },
|
|
2841
|
+
previouslyClaimed: task?.claimedFiles?.length ? task.claimedFiles : null,
|
|
2682
2842
|
message: gitResult?.switched || gitResult?.alreadyOnBranch
|
|
2683
|
-
? `You are on branch "${branch}".
|
|
2843
|
+
? `You are on branch "${branch}". Cursor workspace restored${agentRole ? ` as ${agentRole}` : ''}. Read all skills before editing.`
|
|
2684
2844
|
: `Branch switch failed — see git.manualSteps.`,
|
|
2685
2845
|
nextStep: agentRole === 'builder'
|
|
2686
|
-
?
|
|
2846
|
+
? [
|
|
2847
|
+
`BUILDER role active.`,
|
|
2848
|
+
`1. ⚠️ READ all injected skills (workspace.files) — mandatory before any edits`,
|
|
2849
|
+
`2. Re-claim files: claim_files(taskId="${taskId}", files=${JSON.stringify(task?.claimedFiles || [])})`,
|
|
2850
|
+
`3. Continue implementation from where you left off`,
|
|
2851
|
+
].join('\n')
|
|
2687
2852
|
: agentRole === 'scout'
|
|
2688
2853
|
? `SCOUT role active. Read-only mode — map the codebase and save findings with update_task(scoutReport=...).`
|
|
2689
2854
|
: agentRole === 'coordinator'
|
|
@@ -2693,8 +2858,8 @@ Flow:
|
|
|
2693
2858
|
: task.column === 'in_review'
|
|
2694
2859
|
? `Task is in review. Check if PR feedback needs addressing — call fix_pr_feedback if needed.`
|
|
2695
2860
|
: task.parkNote?.remaining
|
|
2696
|
-
? `Remaining: ${task.parkNote.remaining}
|
|
2697
|
-
: `Continue coding on "${branch}".`,
|
|
2861
|
+
? `Remaining: ${task.parkNote.remaining}. Read workspace.files skills first.`
|
|
2862
|
+
: `Continue coding on "${branch}". Read workspace.files skills first.`,
|
|
2698
2863
|
})
|
|
2699
2864
|
}
|
|
2700
2865
|
)
|
|
@@ -2762,21 +2927,36 @@ The agent files are fully self-contained: each builder knows exactly what MCP to
|
|
|
2762
2927
|
const subtasks = dec.subtasks || []
|
|
2763
2928
|
const execOrder = dec.executionOrder || []
|
|
2764
2929
|
|
|
2765
|
-
//
|
|
2766
|
-
// subtask
|
|
2930
|
+
// Dual-signal completion check — mirrors wait_for_group logic.
|
|
2931
|
+
// A subtask is "done" if EITHER the parent checklist ticks it OR its child task
|
|
2932
|
+
// column is done/in_review. This prevents re-emitting builder prompts for subtasks
|
|
2933
|
+
// that are already finished but whose builder skipped update_task on the parent.
|
|
2767
2934
|
const boardSubtasks = task.subtasks || []
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
const
|
|
2935
|
+
const checklistDone = (title) => boardSubtasks.some(s => s.done && (s.title === title || s.title.endsWith(title)))
|
|
2936
|
+
const childColumnDoneCache = {}
|
|
2937
|
+
const isSubtaskDone = async (title) => {
|
|
2938
|
+
if (checklistDone(title)) return true
|
|
2939
|
+
const entry = subtasks.find(s => s.title === title)
|
|
2940
|
+
if (!entry?.childTaskId) return false
|
|
2941
|
+
if (childColumnDoneCache[entry.childTaskId] !== undefined) return childColumnDoneCache[entry.childTaskId]
|
|
2942
|
+
try {
|
|
2943
|
+
const r = await api.get(`/api/tasks/${entry.childTaskId}`)
|
|
2944
|
+
const col = r?.data?.task?.column
|
|
2945
|
+
const done = col === 'done' || col === 'in_review'
|
|
2946
|
+
childColumnDoneCache[entry.childTaskId] = done
|
|
2947
|
+
return done
|
|
2948
|
+
} catch { return false }
|
|
2949
|
+
}
|
|
2771
2950
|
|
|
2772
2951
|
let nextGroup = null
|
|
2773
2952
|
let nextGroupIndex = -1
|
|
2774
2953
|
for (let i = 0; i < execOrder.length; i++) {
|
|
2775
2954
|
const group = execOrder[i]
|
|
2776
|
-
const allDone = group.
|
|
2955
|
+
const allDone = (await Promise.all(group.map(t => isSubtaskDone(t)))).every(Boolean)
|
|
2777
2956
|
if (!allDone) {
|
|
2778
2957
|
// Also check all prior groups are fully done (respects dependsOn)
|
|
2779
|
-
const
|
|
2958
|
+
const priorResults = await Promise.all(execOrder.slice(0, i).map(g => Promise.all(g.map(t => isSubtaskDone(t)))))
|
|
2959
|
+
const priorAllDone = priorResults.every(g => g.every(Boolean))
|
|
2780
2960
|
if (priorAllDone) {
|
|
2781
2961
|
nextGroup = group
|
|
2782
2962
|
nextGroupIndex = i + 1
|
|
@@ -2792,8 +2972,9 @@ The agent files are fully self-contained: each builder knows exactly what MCP to
|
|
|
2792
2972
|
})
|
|
2793
2973
|
}
|
|
2794
2974
|
|
|
2795
|
-
// Only kick off subtasks that aren't done yet
|
|
2796
|
-
const pendingInGroup = nextGroup.
|
|
2975
|
+
// Only kick off subtasks that aren't done yet (use cached dual-signal results)
|
|
2976
|
+
const pendingInGroup = (await Promise.all(nextGroup.map(async t => ({ t, done: await isSubtaskDone(t) }))))
|
|
2977
|
+
.filter(r => !r.done).map(r => r.t)
|
|
2797
2978
|
const parallelCount = pendingInGroup.length
|
|
2798
2979
|
const isParallel = parallelCount > 1
|
|
2799
2980
|
|
|
@@ -2811,35 +2992,42 @@ The agent files are fully self-contained: each builder knows exactly what MCP to
|
|
|
2811
2992
|
const kickoffs = pendingInGroup.map((subtaskTitle, i) => {
|
|
2812
2993
|
const st = subtasks.find(s => s.title === subtaskTitle) || { title: subtaskTitle, role: 'builder', files: [], description: '' }
|
|
2813
2994
|
const filesArg = (st.files || []).map(f => `"${f}"`).join(', ')
|
|
2814
|
-
const fileList = (st.files || []).map(f => ` - \`${f}\``).join('\n')
|
|
2815
|
-
|
|
2816
|
-
// Prompt is a short, direct imperative Cursor immediately executes.
|
|
2817
|
-
// Key: start with "Use the InternalTool MCP" so Cursor knows to call tools.
|
|
2818
|
-
// Avoid pseudocode like fn(arg=val) — use plain English with quoted values.
|
|
2819
2995
|
const fileListPlain = (st.files || []).join(', ')
|
|
2996
|
+
|
|
2997
|
+
// Use the child task ID if it was embedded at decompose time.
|
|
2998
|
+
// Builders must work on their own child task, not the parent task.
|
|
2999
|
+
// The parent task ID is only used to update the parent checklist when done.
|
|
3000
|
+
const childTaskId = st.childTaskId || taskId // fallback to parent if missing (old decompositions)
|
|
3001
|
+
const childTaskKey = st.childTaskKey || task.key
|
|
3002
|
+
const usingChildTask = !!(st.childTaskId)
|
|
3003
|
+
|
|
2820
3004
|
const prompt = [
|
|
2821
|
-
`Use the InternalTool MCP tools to implement subtask "${st.title}"
|
|
3005
|
+
`Use the InternalTool MCP tools to implement subtask "${st.title}".`,
|
|
3006
|
+
`Your task: ${childTaskKey} (child of ${task.key})`,
|
|
3007
|
+
usingChildTask ? `Child task ID: "${childTaskId}" — use this for ALL MCP calls below.` : `⚠️ No child task ID found — using parent task ID "${taskId}" as fallback.`,
|
|
2822
3008
|
``,
|
|
2823
3009
|
`Do these steps immediately in order — do not ask for confirmation, do not skip any step:`,
|
|
2824
3010
|
``,
|
|
2825
|
-
`1. Call log_session_event with taskId "${
|
|
2826
|
-
`2. Call kickoff_task with taskId "${
|
|
2827
|
-
`
|
|
2828
|
-
`
|
|
3011
|
+
`1. Call log_session_event with taskId "${childTaskId}", type "subagent", name "builder-${i + 1}", summary "Builder ${i + 1} started: ${st.title}"`,
|
|
3012
|
+
`2. Call kickoff_task with taskId "${childTaskId}", agentRole "builder", confirmed true`,
|
|
3013
|
+
` (This writes your cursor workspace. READ every file in .cursor/skills/ before editing anything.)`,
|
|
3014
|
+
`3. Call claim_files with taskId "${childTaskId}" and files [${filesArg}]`,
|
|
3015
|
+
`4. Call get_agent_context with taskId "${childTaskId}" and role "builder" — read the full plan and skills`,
|
|
2829
3016
|
`5. Implement the subtask: ${st.description || st.title}`,
|
|
2830
3017
|
` - You may ONLY modify these files: ${fileListPlain}`,
|
|
2831
|
-
` - Do NOT touch any other file`,
|
|
2832
|
-
` - Follow
|
|
2833
|
-
`6. Run
|
|
2834
|
-
`7. Call commit_helper with taskId "${
|
|
2835
|
-
`8. Call
|
|
2836
|
-
`9. Call
|
|
2837
|
-
`10. Call log_session_event with taskId "${
|
|
3018
|
+
` - Do NOT touch any other file — other builders own those`,
|
|
3019
|
+
` - Follow skills listed in get_agent_context`,
|
|
3020
|
+
`6. Run the project test suite — all tests must pass before continuing`,
|
|
3021
|
+
`7. Call commit_helper with taskId "${childTaskId}"`,
|
|
3022
|
+
`8. Call raise_pr with taskId "${childTaskId}" and projectId "${task.project?._id || task.project}"`,
|
|
3023
|
+
`9. Call update_task with taskId "${taskId}" (parent) and mark the subtask "${st.title}" as done in the subtasks checklist`,
|
|
3024
|
+
`10. Call log_session_event with taskId "${childTaskId}", type "subagent", name "builder-${i + 1}", summary "Builder ${i + 1} finished: ${st.title}"`,
|
|
2838
3025
|
``,
|
|
2839
|
-
`You are builder ${i + 1} of ${parallelCount} running in parallel.
|
|
3026
|
+
`You are builder ${i + 1} of ${parallelCount} running in parallel. Other builders are working on different files simultaneously — do NOT touch their files.`,
|
|
3027
|
+
`If you finish early, call park_task on your child task and notify the coordinator.`,
|
|
2840
3028
|
].join('\n')
|
|
2841
3029
|
|
|
2842
|
-
return { subtask: st.title, role: st.role || 'builder', files: st.files || [], prompt }
|
|
3030
|
+
return { subtask: st.title, role: st.role || 'builder', files: st.files || [], childTaskId, childTaskKey, prompt }
|
|
2843
3031
|
})
|
|
2844
3032
|
|
|
2845
3033
|
// Write each builder prompt as a Cursor Background Agent file in .cursor/agents/
|
|
@@ -2856,14 +3044,14 @@ The agent files are fully self-contained: each builder knows exactly what MCP to
|
|
|
2856
3044
|
|
|
2857
3045
|
const agentFileContent = [
|
|
2858
3046
|
`---`,
|
|
2859
|
-
`description: Builder ${i + 1} of ${parallelCount} — ${k.subtask} (${task.key})`,
|
|
3047
|
+
`description: Builder ${i + 1} of ${parallelCount} — ${k.subtask} (${k.childTaskKey || task.key})`,
|
|
2860
3048
|
`---`,
|
|
2861
3049
|
``,
|
|
2862
3050
|
k.prompt,
|
|
2863
3051
|
].join('\n')
|
|
2864
3052
|
|
|
2865
3053
|
writeFileSync(filePath, agentFileContent, 'utf8')
|
|
2866
|
-
writtenFiles.push({ index: i + 1, file: `.cursor/agents/${fileName}`, subtask: k.subtask })
|
|
3054
|
+
writtenFiles.push({ index: i + 1, file: `.cursor/agents/${fileName}`, subtask: k.subtask, childTaskId: k.childTaskId, childTaskKey: k.childTaskKey })
|
|
2867
3055
|
})
|
|
2868
3056
|
|
|
2869
3057
|
// Build the human-facing instruction block
|
|
@@ -2875,13 +3063,11 @@ The agent files are fully self-contained: each builder knows exactly what MCP to
|
|
|
2875
3063
|
` 2. You'll see ${parallelCount} new builder agent(s) listed`,
|
|
2876
3064
|
` 3. Click "Start" for each one — they run in parallel automatically`,
|
|
2877
3065
|
``,
|
|
2878
|
-
...writtenFiles.map(f => ` • Builder ${f.index}: "${f.subtask}" → ${f.file}`),
|
|
3066
|
+
...writtenFiles.map(f => ` • Builder ${f.index}: "${f.subtask}" [${f.childTaskKey || 'parent'}] → ${f.file}`),
|
|
2879
3067
|
``,
|
|
2880
|
-
`Each builder
|
|
3068
|
+
`Each builder kicks off their own child task → claims files → implements → tests → commits → raises PR.`,
|
|
2881
3069
|
`They work on different files simultaneously — no need to wait for one before starting the other.`,
|
|
2882
|
-
|
|
2883
|
-
`After all builders finish, come back here and call get_parallel_kickoffs again`,
|
|
2884
|
-
`to get the next group's prompts.`,
|
|
3070
|
+
`When all builders in this group are done, call get_parallel_kickoffs again for the next group.`,
|
|
2885
3071
|
].join('\n')
|
|
2886
3072
|
|
|
2887
3073
|
return text({
|
|
@@ -2920,6 +3106,23 @@ Polls every 10 s, times out after 30 min by default. Returns immediately if alre
|
|
|
2920
3106
|
const deadline = Date.now() + timeoutMs
|
|
2921
3107
|
let attempts = 0
|
|
2922
3108
|
|
|
3109
|
+
// Helper: check if a subtask title is done via parent checklist
|
|
3110
|
+
const isCheckedDone = (boardSubtasks, title) =>
|
|
3111
|
+
boardSubtasks.some(s => s.done && (s.title === title || s.title.endsWith(title)))
|
|
3112
|
+
|
|
3113
|
+
// Helper: check if a child task is done via its board column.
|
|
3114
|
+
// A child task is considered done when it reaches 'done' or 'in_review'
|
|
3115
|
+
// (builder finished + raised PR). This is the reliable signal — builders
|
|
3116
|
+
// may skip calling update_task on the parent but they always raise_pr.
|
|
3117
|
+
const isChildTaskDone = async (childTaskId) => {
|
|
3118
|
+
if (!childTaskId) return false
|
|
3119
|
+
try {
|
|
3120
|
+
const res = await api.get(`/api/tasks/${childTaskId}`)
|
|
3121
|
+
const col = res?.data?.task?.column
|
|
3122
|
+
return col === 'done' || col === 'in_review'
|
|
3123
|
+
} catch { return false }
|
|
3124
|
+
}
|
|
3125
|
+
|
|
2923
3126
|
while (Date.now() < deadline) {
|
|
2924
3127
|
attempts++
|
|
2925
3128
|
const taskRes = await apiWithRetry(() => api.get(`/api/tasks/${taskId}`))
|
|
@@ -2932,27 +3135,43 @@ Polls every 10 s, times out after 30 min by default. Returns immediately if alre
|
|
|
2932
3135
|
return errorText('Decomposition JSON is malformed.')
|
|
2933
3136
|
}
|
|
2934
3137
|
|
|
2935
|
-
const execOrder
|
|
3138
|
+
const execOrder = dec.executionOrder || []
|
|
3139
|
+
const decSubtasks = dec.subtasks || []
|
|
2936
3140
|
const targetGroup = execOrder[groupIndex - 1]
|
|
2937
3141
|
if (!targetGroup) return errorText(`Group ${groupIndex} does not exist. Decomposition has ${execOrder.length} group(s).`)
|
|
2938
3142
|
|
|
2939
3143
|
const boardSubtasks = task.subtasks || []
|
|
2940
|
-
const isDone = (title) => boardSubtasks.some(s => s.done && (s.title === title || s.title.endsWith(title)))
|
|
2941
3144
|
|
|
2942
|
-
|
|
2943
|
-
|
|
3145
|
+
// Dual-signal completion: parent checklist tick OR child task column (done/in_review).
|
|
3146
|
+
// This prevents the coordinator from stalling when a builder skips update_task on the parent.
|
|
3147
|
+
const statusPerTitle = await Promise.all(
|
|
3148
|
+
targetGroup.map(async (title) => {
|
|
3149
|
+
const checklist = isCheckedDone(boardSubtasks, title)
|
|
3150
|
+
if (checklist) return { title, done: true, via: 'checklist' }
|
|
3151
|
+
// Look up child task ID from the embedded decomposition
|
|
3152
|
+
const decEntry = decSubtasks.find(s => s.title === title)
|
|
3153
|
+
const childDone = decEntry?.childTaskId
|
|
3154
|
+
? await isChildTaskDone(decEntry.childTaskId)
|
|
3155
|
+
: false
|
|
3156
|
+
return { title, done: childDone, via: childDone ? 'child_task_column' : 'pending', childTaskId: decEntry?.childTaskId || null }
|
|
3157
|
+
})
|
|
3158
|
+
)
|
|
3159
|
+
|
|
3160
|
+
const doneTitles = statusPerTitle.filter(s => s.done)
|
|
3161
|
+
const pendingTitles = statusPerTitle.filter(s => !s.done)
|
|
2944
3162
|
|
|
2945
3163
|
if (pendingTitles.length === 0) {
|
|
2946
3164
|
return text({
|
|
2947
|
-
done:
|
|
2948
|
-
group:
|
|
2949
|
-
subtasks:
|
|
3165
|
+
done: true,
|
|
3166
|
+
group: groupIndex,
|
|
3167
|
+
subtasks: targetGroup,
|
|
2950
3168
|
done_count: doneTitles.length,
|
|
3169
|
+
completion: doneTitles.map(s => ({ title: s.title, via: s.via })),
|
|
2951
3170
|
attempts,
|
|
2952
3171
|
message: `✅ All ${targetGroup.length} subtask(s) in Group ${groupIndex} are done after ${attempts} poll(s).`,
|
|
2953
3172
|
nextStep: groupIndex < execOrder.length
|
|
2954
3173
|
? `Call get_parallel_kickoffs with taskId="${taskId}" to get Group ${groupIndex + 1}'s builder prompts.`
|
|
2955
|
-
: 'All groups complete. Run final tests then call raise_pr.',
|
|
3174
|
+
: 'All groups complete. Run final tests then call raise_pr on the parent task.',
|
|
2956
3175
|
})
|
|
2957
3176
|
}
|
|
2958
3177
|
|
|
@@ -2975,10 +3194,17 @@ Polls every 10 s, times out after 30 min by default. Returns immediately if alre
|
|
|
2975
3194
|
const finalTask = finalRes?.data?.task
|
|
2976
3195
|
let finalDec
|
|
2977
3196
|
try { finalDec = JSON.parse(finalTask?.decomposition || '{}') } catch { finalDec = {} }
|
|
2978
|
-
const finalGroup
|
|
2979
|
-
const
|
|
2980
|
-
const
|
|
2981
|
-
const stillPending
|
|
3197
|
+
const finalGroup = (finalDec.executionOrder || [])[groupIndex - 1] || []
|
|
3198
|
+
const finalDecSubs = finalDec.subtasks || []
|
|
3199
|
+
const finalBoard = finalTask?.subtasks || []
|
|
3200
|
+
const stillPending = await Promise.all(
|
|
3201
|
+
finalGroup.map(async (title) => {
|
|
3202
|
+
if (isCheckedDone(finalBoard, title)) return null
|
|
3203
|
+
const entry = finalDecSubs.find(s => s.title === title)
|
|
3204
|
+
if (entry?.childTaskId && await isChildTaskDone(entry.childTaskId)) return null
|
|
3205
|
+
return title
|
|
3206
|
+
})
|
|
3207
|
+
).then(r => r.filter(Boolean))
|
|
2982
3208
|
|
|
2983
3209
|
return text({
|
|
2984
3210
|
timedOut: true,
|
|
@@ -2986,7 +3212,7 @@ Polls every 10 s, times out after 30 min by default. Returns immediately if alre
|
|
|
2986
3212
|
pending: stillPending,
|
|
2987
3213
|
attempts,
|
|
2988
3214
|
message: `⏱ Timed out after ${attempts} poll(s). ${stillPending.length} subtask(s) still pending: ${stillPending.join(', ')}`,
|
|
2989
|
-
hint: 'Check the Background Agents panel — a builder may have crashed. Call wait_for_group again to resume
|
|
3215
|
+
hint: 'Check the Background Agents panel — a builder may have crashed or skipped raise_pr. Call wait_for_group again to resume, or proceed manually if the work is done.',
|
|
2990
3216
|
})
|
|
2991
3217
|
}
|
|
2992
3218
|
)
|
|
@@ -3808,6 +4034,18 @@ Use this when a developer says "start task", "brief me on", or "what do I need t
|
|
|
3808
4034
|
const hasParallelLang = /parallel|simultaneously|independent(ly)?|at the same time/i.test(readme)
|
|
3809
4035
|
const hasSequentialLang = /step\s*\d|first[\s,].*then|depends\s+on|before.*after/i.test(readme)
|
|
3810
4036
|
|
|
4037
|
+
// Count how many distinct codebase layers are touched: routes, models, components, services, tests, migrations, seeds, utils, middleware, hooks
|
|
4038
|
+
const LAYER_PATTERNS = ['route', 'model', 'component', 'service', 'test', 'migration', 'seed', 'util', 'middleware', 'hook', 'schema', 'store', 'context', 'controller', 'resolver']
|
|
4039
|
+
const layersHit = LAYER_PATTERNS.filter(l => new RegExp(l, 'i').test(readme)).length
|
|
4040
|
+
|
|
4041
|
+
// Count explicit numbered steps / subtasks in the plan (indicates decomposability)
|
|
4042
|
+
const numberedSteps = (readme.match(/^\s*\d+\.\s/gm) || []).length
|
|
4043
|
+
|
|
4044
|
+
// Full-stack scope: touches both frontend and backend layers
|
|
4045
|
+
const hasFrontend = /component|hook|store|context|jsx|tsx|vue|react|svelte|ui|page|view/i.test(readme)
|
|
4046
|
+
const hasBackend = /route|controller|model|schema|service|middleware|api|endpoint|database|migration/i.test(readme)
|
|
4047
|
+
const isFullStack = hasFrontend && hasBackend
|
|
4048
|
+
|
|
3811
4049
|
if (readmeLen > 800) { complexityScore += 2; complexitySignals.push(`long plan (${readmeLen} chars)`) }
|
|
3812
4050
|
else if (readmeLen > 300) { complexityScore += 1; complexitySignals.push(`medium plan (${readmeLen} chars)`) }
|
|
3813
4051
|
if (sections > 3) { complexityScore += 2; complexitySignals.push(`${sections} README sections`) }
|
|
@@ -3816,13 +4054,23 @@ Use this when a developer says "start task", "brief me on", or "what do I need t
|
|
|
3816
4054
|
else if (fileMentions > 2) { complexityScore += 1; complexitySignals.push(`${fileMentions} files mentioned`) }
|
|
3817
4055
|
if (hasParallelLang) { complexityScore += 1; complexitySignals.push('parallel workstreams mentioned') }
|
|
3818
4056
|
if (hasSequentialLang) { complexityScore += 1; complexitySignals.push('sequential steps mentioned') }
|
|
4057
|
+
if (layersHit >= 4) { complexityScore += 2; complexitySignals.push(`${layersHit} codebase layers touched`) }
|
|
4058
|
+
else if (layersHit >= 2) { complexityScore += 1; complexitySignals.push(`${layersHit} codebase layers touched`) }
|
|
4059
|
+
if (numberedSteps >= 5) { complexityScore += 2; complexitySignals.push(`${numberedSteps} explicit steps in plan`) }
|
|
4060
|
+
else if (numberedSteps >= 3) { complexityScore += 1; complexitySignals.push(`${numberedSteps} steps in plan`) }
|
|
4061
|
+
if (isFullStack) { complexityScore += 1; complexitySignals.push('full-stack scope (frontend + backend)') }
|
|
3819
4062
|
}
|
|
3820
4063
|
|
|
4064
|
+
// Score from task-level signals (subtask count from board)
|
|
4065
|
+
const existingSubtaskCount = (task.subtasks || []).length
|
|
4066
|
+
if (existingSubtaskCount >= 5) { complexityScore += 2; complexitySignals.push(`${existingSubtaskCount} subtasks already defined`) }
|
|
4067
|
+
else if (existingSubtaskCount >= 3) { complexityScore += 1; complexitySignals.push(`${existingSubtaskCount} subtasks defined`) }
|
|
4068
|
+
|
|
3821
4069
|
const titleLower = task.title.toLowerCase()
|
|
3822
|
-
const complexTitleWords = ['implement', 'build', 'add', 'create', 'integrate', 'refactor', 'migrate', 'redesign']
|
|
3823
|
-
const simpleTitleWords = ['fix', 'patch', 'typo', 'bump', 'revert']
|
|
3824
|
-
if (complexTitleWords.some(w => titleLower.includes(w))) complexityScore += 1
|
|
3825
|
-
if (simpleTitleWords.some(w => titleLower.includes(w))) complexityScore -= 1
|
|
4070
|
+
const complexTitleWords = ['implement', 'build', 'add', 'create', 'integrate', 'refactor', 'migrate', 'redesign', 'replace', 'overhaul', 'rearchitect']
|
|
4071
|
+
const simpleTitleWords = ['fix', 'patch', 'typo', 'bump', 'revert', 'update', 'rename', 'remove']
|
|
4072
|
+
if (complexTitleWords.some(w => titleLower.includes(w))) { complexityScore += 1; complexitySignals.push('complex verb in title') }
|
|
4073
|
+
if (simpleTitleWords.some(w => titleLower.includes(w))) { complexityScore -= 1 }
|
|
3826
4074
|
complexityScore = Math.max(0, complexityScore)
|
|
3827
4075
|
|
|
3828
4076
|
// ── Determine recommended flow ────────────────────────────────────────────
|
|
@@ -3888,6 +4136,27 @@ Use this when a developer says "start task", "brief me on", or "what do I need t
|
|
|
3888
4136
|
}
|
|
3889
4137
|
}
|
|
3890
4138
|
|
|
4139
|
+
// ── Fetch team landscape for the preview so agent sees conflicts before committing ──
|
|
4140
|
+
let previewTeamContext = null
|
|
4141
|
+
try {
|
|
4142
|
+
const previewTreeRes = await api.get(`/api/projects/${task.project}/github/git-tree`).catch(() => null)
|
|
4143
|
+
if (previewTreeRes?.success) {
|
|
4144
|
+
const branches = previewTreeRes.data?.branches || []
|
|
4145
|
+
const otherActive = branches.filter(b => b.taskId !== String(task._id) && !b.branchError)
|
|
4146
|
+
if (otherActive.length > 0) {
|
|
4147
|
+
previewTeamContext = {
|
|
4148
|
+
activeBranches: otherActive.map(b => ({
|
|
4149
|
+
taskKey: b.taskKey,
|
|
4150
|
+
assignees: (b.assignees || []).map(a => a.name).join(', ') || 'unassigned',
|
|
4151
|
+
files: b.claimedFiles || [],
|
|
4152
|
+
behindBy: b.compare?.behindBy ?? null,
|
|
4153
|
+
})),
|
|
4154
|
+
warning: `⚠️ ${otherActive.length} other branch(es) active in this project. Check for file overlaps before claiming files.`,
|
|
4155
|
+
}
|
|
4156
|
+
}
|
|
4157
|
+
}
|
|
4158
|
+
} catch { /* non-fatal */ }
|
|
4159
|
+
|
|
3891
4160
|
// ── Preview: show the full plan before touching anything ──
|
|
3892
4161
|
const pendingApv = (task.approvals || []).find(a => a.state === 'pending') || null
|
|
3893
4162
|
const hasApprovedApv = (task.approvals || []).some(a => a.state === 'approved')
|
|
@@ -4120,6 +4389,7 @@ Use this when a developer says "start task", "brief me on", or "what do I need t
|
|
|
4120
4389
|
mainWarning: `⚠️ Your workspace is ${localGitState.mainBehindBy} commit(s) behind origin/${localGitState.defaultBranch}. Run: git pull origin ${localGitState.defaultBranch} before creating a branch.`,
|
|
4121
4390
|
}),
|
|
4122
4391
|
} : null,
|
|
4392
|
+
teamLandscape: previewTeamContext,
|
|
4123
4393
|
requiresConfirmation: true,
|
|
4124
4394
|
message: approvalBlocks
|
|
4125
4395
|
? `Read the plan above, then follow workflowRoadmap — approval is required before you can branch and start coding.`
|
|
@@ -4471,21 +4741,85 @@ After \`request_human_input\`: STOP, show the question in chat, wait for reply,
|
|
|
4471
4741
|
scoutFirst: taskTypeCfg.scoutFirst,
|
|
4472
4742
|
typeRulesInjected: !!typeExtraRules,
|
|
4473
4743
|
} : null,
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4744
|
+
mandatoryWorkflow: (() => {
|
|
4745
|
+
const flow = recommendedFlow // 'direct' | 'single_builder' | 'coordinator'
|
|
4746
|
+
const role = agentRole || 'builder'
|
|
4747
|
+
|
|
4748
|
+
// ── Coordinator flow ──────────────────────────────────────────────────
|
|
4749
|
+
if (role === 'coordinator') {
|
|
4750
|
+
return {
|
|
4751
|
+
flow: 'coordinator',
|
|
4752
|
+
WARNING: '⛔ COORDINATOR MODE — DO NOT WRITE ANY CODE. Your only job is to plan and delegate.',
|
|
4753
|
+
mandatorySteps: [
|
|
4754
|
+
'1. READ all files in .cursor/skills/ — log each with log_session_event(type="skill", name=<filename>)',
|
|
4755
|
+
'2. Call recall(taskId) to restore memory from previous sessions',
|
|
4756
|
+
'3. Call get_agent_context(taskId, role="coordinator") for the full context',
|
|
4757
|
+
`4. If no scout report exists: call scout_task(taskId="${taskId}", confirmed=false) to get the analysis brief, then scout_task(confirmed=true, report="<findings>") to save it — do NOT switch agentRole to scout`,
|
|
4758
|
+
'5. Call decompose_task(confirmed=false) — preview the subtask breakdown with file ownership',
|
|
4759
|
+
'6. Fix any file overlap conflicts, then call decompose_task(confirmed=true)',
|
|
4760
|
+
'7. Call get_parallel_kickoffs — it writes builder agent files automatically',
|
|
4761
|
+
'8. Tell the user: open Background Agents panel (⌘⇧J) and start each builder',
|
|
4762
|
+
'9. DO NOT start building. DO NOT claim files. DO NOT edit code.',
|
|
4763
|
+
],
|
|
4764
|
+
blockedUntil: 'decompose_task confirmed=true is called and childTasks are created',
|
|
4765
|
+
}
|
|
4766
|
+
}
|
|
4767
|
+
|
|
4768
|
+
// ── Scout flow ────────────────────────────────────────────────────────
|
|
4769
|
+
if (role === 'scout') {
|
|
4770
|
+
return {
|
|
4771
|
+
flow: 'scout',
|
|
4772
|
+
WARNING: '⛔ SCOUT MODE — READ ONLY. DO NOT write or modify any source file.',
|
|
4773
|
+
mandatorySteps: [
|
|
4774
|
+
'1. READ all files in .cursor/skills/ — log each with log_session_event(type="skill", name=<filename>)',
|
|
4775
|
+
'2. Call recall(taskId) to restore memory from previous sessions',
|
|
4776
|
+
'3. Call scout_task(confirmed=false) to get your analysis brief',
|
|
4777
|
+
'4. Read every relevant source file systematically (routes, models, utils, tests)',
|
|
4778
|
+
'5. Call scout_task(confirmed=true, report="<your findings>") to save the report',
|
|
4779
|
+
'6. DO NOT modify any file. DO NOT create branches. DO NOT commit.',
|
|
4780
|
+
],
|
|
4781
|
+
blockedUntil: 'scout_task confirmed=true with a non-empty report',
|
|
4782
|
+
}
|
|
4783
|
+
}
|
|
4784
|
+
|
|
4785
|
+
// ── Reviewer flow ─────────────────────────────────────────────────────
|
|
4786
|
+
if (role === 'reviewer') {
|
|
4787
|
+
return {
|
|
4788
|
+
flow: 'reviewer',
|
|
4789
|
+
mandatorySteps: [
|
|
4790
|
+
'1. READ all files in .cursor/skills/ — log each with log_session_event(type="skill", name=<filename>)',
|
|
4791
|
+
'2. Call review_pr — read the FULL diff before forming any opinion',
|
|
4792
|
+
'3. Check every changed file line by line against the implementation plan',
|
|
4793
|
+
'4. Call post_pr_review with verdict, analysisPoints (min 2), and specific file+line comments',
|
|
4794
|
+
],
|
|
4795
|
+
blockedUntil: 'post_pr_review called with verdict',
|
|
4796
|
+
}
|
|
4797
|
+
}
|
|
4798
|
+
|
|
4799
|
+
// ── Builder flow (direct / single_builder / decompose child) ──────────
|
|
4800
|
+
const inDecomposeFlow = !!(task.decomposition?.trim())
|
|
4801
|
+
return {
|
|
4802
|
+
flow: inDecomposeFlow ? 'decompose_builder' : flow,
|
|
4803
|
+
SKILLS_GATE: '⛔ DO NOT EDIT ANY FILE until all steps 1–3 are complete.',
|
|
4804
|
+
mandatorySteps: [
|
|
4805
|
+
'1. READ all files in .cursor/skills/ — log each: log_session_event(taskId, type="skill", name=<skill-name>, role="builder")',
|
|
4806
|
+
'2. Call recall(taskId) to restore memory — check for prior session findings',
|
|
4807
|
+
'3. Call get_agent_context(taskId, role="builder") — read implementation plan + claimed files',
|
|
4808
|
+
alreadyHasBranch
|
|
4809
|
+
? `4. You are on branch "${task.github.headBranch}" — run: git pull origin ${task.github.headBranch}`
|
|
4810
|
+
: `4. Call create_branch — creates "${suggestedBranch}" and moves task to in_progress`,
|
|
4811
|
+
'5. Call claim_files with your exact file list — REQUIRED before editing',
|
|
4812
|
+
'6. Implement the feature following the plan. Run tests after each logical unit.',
|
|
4813
|
+
'7. Call commit_helper to stage and commit with a conventional message',
|
|
4814
|
+
'8. Call raise_pr when all subtasks are done and tests pass',
|
|
4815
|
+
],
|
|
4816
|
+
blockedUntil: 'Skills read (step 1), memory recalled (step 2), files claimed (step 5)',
|
|
4817
|
+
inDecomposeFlow,
|
|
4818
|
+
decomposeNote: inDecomposeFlow
|
|
4819
|
+
? '⚡ This is a PARALLEL BUILD. Only edit your claimed files. Do NOT touch files owned by other subtasks.'
|
|
4820
|
+
: null,
|
|
4821
|
+
}
|
|
4822
|
+
})(),
|
|
4489
4823
|
})
|
|
4490
4824
|
}
|
|
4491
4825
|
)
|
|
@@ -4744,15 +5078,54 @@ IMPORTANT — what to write in "summary":
|
|
|
4744
5078
|
|
|
4745
5079
|
server.tool(
|
|
4746
5080
|
'decide_task_approval',
|
|
4747
|
-
|
|
5081
|
+
`Approve or reject a specific approval request by approvalId. Only the designated reviewer can call this.
|
|
5082
|
+
|
|
5083
|
+
BEFORE calling with a decision, review the plan:
|
|
5084
|
+
- Read the plan summary returned by this tool (confirmed=false)
|
|
5085
|
+
- Read the task comments for any context from the submitter
|
|
5086
|
+
- Check the implementation plan for completeness, correctness, and feasibility
|
|
5087
|
+
- Approve if the plan is clear and actionable; reject with a specific note explaining what needs to change
|
|
5088
|
+
|
|
5089
|
+
Set confirmed=false first to read the plan, then call again with confirmed=true and your decision.`,
|
|
4748
5090
|
{
|
|
4749
5091
|
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
4750
5092
|
approvalId: z.string().describe("Approval request's MongoDB ObjectId (from the task's approvals array)"),
|
|
4751
|
-
decision: z.enum(['approve', 'reject']),
|
|
4752
|
-
note: z.string().optional().describe('Reason for the decision'),
|
|
5093
|
+
decision: z.enum(['approve', 'reject']).optional().describe('Your decision — omit to just preview the plan'),
|
|
5094
|
+
note: z.string().optional().describe('Reason for the decision (required on reject, recommended on approve)'),
|
|
5095
|
+
confirmed: z.boolean().optional().default(false).describe('Set true to submit the decision after reviewing the plan'),
|
|
4753
5096
|
},
|
|
4754
|
-
async ({ taskId, approvalId, decision, note }) =>
|
|
4755
|
-
|
|
5097
|
+
async ({ taskId, approvalId, decision, note, confirmed = false }) => {
|
|
5098
|
+
// Always fetch task so reviewer can read the plan before deciding
|
|
5099
|
+
const taskRes = await api.get(`/api/tasks/${taskId}`)
|
|
5100
|
+
const task = taskRes?.data?.task
|
|
5101
|
+
if (!task) return errorText('Task not found')
|
|
5102
|
+
|
|
5103
|
+
const approval = (task.approvals || []).find(a => String(a._id) === String(approvalId))
|
|
5104
|
+
if (!approval) return errorText('Approval request not found on this task')
|
|
5105
|
+
|
|
5106
|
+
if (!confirmed || !decision) {
|
|
5107
|
+
// Preview mode — return plan so reviewer can read it first
|
|
5108
|
+
const planLines = (approval.readme || '').split('\n')
|
|
5109
|
+
const planPreview = planLines.slice(0, 80).join('\n') + (planLines.length > 80 ? '\n\n[…plan truncated, first 80 lines shown]' : '')
|
|
5110
|
+
return text({
|
|
5111
|
+
task: { key: task.key, title: task.title, priority: task.priority },
|
|
5112
|
+
approval: { id: approval._id, title: approval.title, state: approval.state,
|
|
5113
|
+
submittedBy: approval.requestedBy?.name || approval.requestedBy?.email || 'unknown',
|
|
5114
|
+
submittedAt: approval.requestedAt },
|
|
5115
|
+
planPreview,
|
|
5116
|
+
reviewChecklist: [
|
|
5117
|
+
'Is the plan clear and specific? (not vague high-level bullets)',
|
|
5118
|
+
'Does it identify the correct files/modules to change?',
|
|
5119
|
+
'Does it handle edge cases and error paths?',
|
|
5120
|
+
'Is the scope reasonable for a single task?',
|
|
5121
|
+
'Any security, data-loss, or regression risks?',
|
|
5122
|
+
],
|
|
5123
|
+
message: `Review the plan above, then call decide_task_approval again with confirmed=true and decision="approve" or "reject".`,
|
|
5124
|
+
})
|
|
5125
|
+
}
|
|
5126
|
+
|
|
5127
|
+
return call(() => api.post(`/api/tasks/${taskId}/approvals/${approvalId}/decide`, { decision, note }))
|
|
5128
|
+
}
|
|
4756
5129
|
)
|
|
4757
5130
|
}
|
|
4758
5131
|
|
|
@@ -5466,6 +5839,47 @@ function writeCursorWorkspace(task, projectAgentConfig, startPath) {
|
|
|
5466
5839
|
|
|
5467
5840
|
const written = []
|
|
5468
5841
|
|
|
5842
|
+
// ── 0. Session protocol rule — always-apply, enforces skill gate ──────────
|
|
5843
|
+
// This is the first rule Cursor sees (filename 00- sorts before all others).
|
|
5844
|
+
// It forces the agent to read skills before editing — the root cause of
|
|
5845
|
+
// builders skipping the skill step was the lack of a hard runtime constraint.
|
|
5846
|
+
const sessionProtocolRule = `---
|
|
5847
|
+
description: InternalTool session protocol — enforced for every agent session on this task.
|
|
5848
|
+
alwaysApply: true
|
|
5849
|
+
---
|
|
5850
|
+
|
|
5851
|
+
# ⛔ MANDATORY SESSION PROTOCOL
|
|
5852
|
+
|
|
5853
|
+
**You MUST follow these steps IN ORDER before editing any file.**
|
|
5854
|
+
Skipping any step is a protocol violation and will result in incorrect implementation.
|
|
5855
|
+
|
|
5856
|
+
## Step 1 — Read all skills (REQUIRED before any Edit/Write)
|
|
5857
|
+
For each file in \`.cursor/skills/\`:
|
|
5858
|
+
1. Read the file completely
|
|
5859
|
+
2. Call \`log_session_event\` with type="skill", name=<filename without .md>, role=<your role>
|
|
5860
|
+
|
|
5861
|
+
Do NOT call Edit or Write until every skill file has been read and logged.
|
|
5862
|
+
|
|
5863
|
+
## Step 2 — Recall memory
|
|
5864
|
+
Call \`recall(taskId="${taskId}")\` to restore facts from previous sessions.
|
|
5865
|
+
If memory contains a \`status\` key, read it — the task may have known blockers.
|
|
5866
|
+
|
|
5867
|
+
## Step 3 — Get full context
|
|
5868
|
+
Call \`get_agent_context(taskId="${taskId}", role=<your role>)\`.
|
|
5869
|
+
Read the implementation plan, claimed files, and scout report before proceeding.
|
|
5870
|
+
|
|
5871
|
+
## Step 4 — Claim files (builders only)
|
|
5872
|
+
Call \`claim_files\` with every file you will edit BEFORE your first Edit call.
|
|
5873
|
+
Editing unclaimed files causes merge conflicts with other agents.
|
|
5874
|
+
|
|
5875
|
+
---
|
|
5876
|
+
|
|
5877
|
+
**Role-specific hard constraints are in \`.cursor/agents/active-agent.md\`.**
|
|
5878
|
+
**Project-level rules are in \`.cursor/rules/\` (other .mdc files).**
|
|
5879
|
+
`
|
|
5880
|
+
writeFileSync(join(rulesDir, '00-session-protocol.mdc'), sessionProtocolRule, 'utf8')
|
|
5881
|
+
written.push(join(rulesDir, '00-session-protocol.mdc'))
|
|
5882
|
+
|
|
5469
5883
|
// ── 1. Project-level rules from DB → .cursor/rules/<name>.mdc ─────────────
|
|
5470
5884
|
const projectRules = cfg.rules || []
|
|
5471
5885
|
for (const r of projectRules) {
|
|
@@ -5874,7 +6288,17 @@ async function apiWithRetry(fn, maxRetries = 2, initialDelay = 500) {
|
|
|
5874
6288
|
}
|
|
5875
6289
|
|
|
5876
6290
|
function wrapApiError(e) {
|
|
5877
|
-
|
|
6291
|
+
const cause = e._cause || 'unknown'
|
|
6292
|
+
const isAuth = cause === 'auth'
|
|
6293
|
+
return {
|
|
6294
|
+
error: true,
|
|
6295
|
+
cause,
|
|
6296
|
+
message: e.message?.split('\n')[0],
|
|
6297
|
+
...(isAuth ? {
|
|
6298
|
+
authError: true,
|
|
6299
|
+
fix: 'Your session token has expired or is invalid. Ask the developer to restart the MCP server (which re-reads the INTERNALTOOL_API_KEY env var) or re-run: npx internaltool-mcp to reconnect.',
|
|
6300
|
+
} : {}),
|
|
6301
|
+
}
|
|
5878
6302
|
}
|
|
5879
6303
|
|
|
5880
6304
|
// ── #9 Get last git commit metadata ──────────────────────────────────────────
|
|
@@ -6427,7 +6851,16 @@ If you have uncommitted tracked changes, it will tell you exactly what to do bef
|
|
|
6427
6851
|
if (needsApproval) {
|
|
6428
6852
|
const isFix2 = /\b(fix|bug|hotfix|patch)\b/i.test(task.title + ' ' + (task.description || ''))
|
|
6429
6853
|
const slug2 = task.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 35)
|
|
6430
|
-
|
|
6854
|
+
// Fetch devSlug here too so the preview matches the actual branch name
|
|
6855
|
+
let devSlug2 = ''
|
|
6856
|
+
try {
|
|
6857
|
+
const meRes2 = await api.get('/api/auth/me')
|
|
6858
|
+
const devName2 = meRes2?.data?.user?.name || meRes2?.data?.name || ''
|
|
6859
|
+
devSlug2 = devName2.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 15)
|
|
6860
|
+
} catch { /* non-fatal */ }
|
|
6861
|
+
const previewBranch = devSlug2
|
|
6862
|
+
? `${devSlug2}/${isFix2 ? 'fix' : 'feature'}/${task.key.toLowerCase()}-${slug2}`
|
|
6863
|
+
: `${isFix2 ? 'fix' : 'feature'}/${task.key.toLowerCase()}-${slug2}`
|
|
6431
6864
|
return text({
|
|
6432
6865
|
blocked: true,
|
|
6433
6866
|
reason: 'approval_required',
|
|
@@ -6833,7 +7266,7 @@ Set confirmed=false first to preview the full plan, then confirmed=true to save
|
|
|
6833
7266
|
})
|
|
6834
7267
|
}
|
|
6835
7268
|
|
|
6836
|
-
// Park Task B if provided
|
|
7269
|
+
// Park Task B if provided — and clean up its workspace so skills don't bleed into Task A
|
|
6837
7270
|
if (taskB && taskBId) {
|
|
6838
7271
|
try {
|
|
6839
7272
|
await api.patch(`/api/tasks/${taskBId}/park`, {
|
|
@@ -6842,6 +7275,14 @@ Set confirmed=false first to preview the full plan, then confirmed=true to save
|
|
|
6842
7275
|
blockers: '',
|
|
6843
7276
|
})
|
|
6844
7277
|
} catch { /* non-fatal — developer can park manually */ }
|
|
7278
|
+
// Delete Task B's cursor workspace — same cleanup that park_task does.
|
|
7279
|
+
// Without this, Task B's skills and session-protocol stay on disk while
|
|
7280
|
+
// working on Task A's branch, and the agent may read the wrong context.
|
|
7281
|
+
try { deleteCursorWorkspace(taskB?.agentRole || null, process.cwd()) } catch { /* non-fatal */ }
|
|
7282
|
+
try { unlinkSync(join(process.cwd(), '.internaltool-active-task')) } catch { /* non-fatal */ }
|
|
7283
|
+
api.patch(`/api/tasks/${taskBId}`, {
|
|
7284
|
+
agentWorkspace: { clearedAt: new Date().toISOString() }
|
|
7285
|
+
}).catch(() => {})
|
|
6845
7286
|
}
|
|
6846
7287
|
|
|
6847
7288
|
return text({
|
|
@@ -7013,21 +7454,33 @@ Set confirmed=false first to preview the full PR content, then confirmed=true to
|
|
|
7013
7454
|
})
|
|
7014
7455
|
if (!res?.success) return errorText(res?.message || 'Could not create PR')
|
|
7015
7456
|
|
|
7016
|
-
// Delete the
|
|
7457
|
+
// Delete the full cursor workspace (skills, rules, active-agent) — coding is done
|
|
7017
7458
|
const deletedRulesFile = deleteCursorRulesFile(task.key, repoPath)
|
|
7459
|
+
deleteCursorWorkspace(task?.agentRole || null, repoPath || process.cwd())
|
|
7460
|
+
|
|
7461
|
+
// Move task to in_review directly — don't rely solely on GitHub webhook
|
|
7462
|
+
// (webhook may not be configured, or may be delayed)
|
|
7463
|
+
try {
|
|
7464
|
+
await api.post(`/api/tasks/${taskId}/move`, { column: 'in_review', toIndex: 0 })
|
|
7465
|
+
} catch { /* non-fatal — webhook will do it if direct patch fails */ }
|
|
7466
|
+
|
|
7467
|
+
// Remove .internaltool-active-task marker
|
|
7468
|
+
try { unlinkSync(join(process.cwd(), '.internaltool-active-task')) } catch { /* non-fatal */ }
|
|
7018
7469
|
|
|
7019
7470
|
return text({
|
|
7020
7471
|
prNumber: res.data.prNumber,
|
|
7021
7472
|
prUrl: res.data.prUrl,
|
|
7022
7473
|
title: prTitle,
|
|
7023
7474
|
draft,
|
|
7024
|
-
message: `PR #${res.data.prNumber} created.`,
|
|
7025
|
-
|
|
7026
|
-
|
|
7027
|
-
:
|
|
7475
|
+
message: `PR #${res.data.prNumber} created. Task moved to In Review.`,
|
|
7476
|
+
cursorWorkspaceCleared: {
|
|
7477
|
+
cleared: true,
|
|
7478
|
+
rulesFile: deletedRulesFile || null,
|
|
7479
|
+
note: 'Cursor workspace (skills + rules + active-agent) deleted — coding session is complete.',
|
|
7480
|
+
},
|
|
7028
7481
|
nextStep: draft
|
|
7029
7482
|
? 'PR is a draft. Mark it ready for review on GitHub when you want reviewer notifications to fire.'
|
|
7030
|
-
:
|
|
7483
|
+
: `PR is live at ${res.data.prUrl}. Task is now in_review — the reviewer can call kickoff_task with agentRole="reviewer".`,
|
|
7031
7484
|
})
|
|
7032
7485
|
} catch (e) {
|
|
7033
7486
|
return errorText(e.message)
|
|
@@ -7762,12 +8215,13 @@ Use this when a developer asks: "what do I do?", "how do I rebase?", "I have con
|
|
|
7762
8215
|
} else {
|
|
7763
8216
|
steps.push({
|
|
7764
8217
|
step: stepNum++,
|
|
7765
|
-
title: `
|
|
7766
|
-
why: `Your PR is already open. After pushing the rebase,
|
|
8218
|
+
title: `Get your PR reviewed — PR #${branch.prNumber}`,
|
|
8219
|
+
why: `Your PR is already open. After pushing the rebase, ask your reviewer to approve so it can be merged.`,
|
|
7767
8220
|
commands: [
|
|
7768
|
-
{ label: '
|
|
8221
|
+
{ label: 'Open the PR in your browser to add reviewers', cmd: `gh pr view --web ${branch.prNumber}` },
|
|
8222
|
+
{ label: 'Or check PR status in the terminal', cmd: `gh pr status` },
|
|
7769
8223
|
],
|
|
7770
|
-
note: `PR URL: ${branch.prUrl || 'see GitHub'}
|
|
8224
|
+
note: `PR URL: ${branch.prUrl || 'see GitHub'}. Add reviewers on GitHub (top-right "Reviewers" panel) — do NOT use "gh pr review --request-changes" (that submits a rejection verdict, not a review request).`,
|
|
7771
8225
|
})
|
|
7772
8226
|
}
|
|
7773
8227
|
|
|
@@ -8170,7 +8624,7 @@ Use this to remember decisions, constraints, context, and findings that should p
|
|
|
8170
8624
|
Call recall(taskId) to retrieve everything at any time.`,
|
|
8171
8625
|
{
|
|
8172
8626
|
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
8173
|
-
key: z.string().describe('Memory key — short, descriptive, no spaces (e.g. "arch_decision", "constraint_auth")'),
|
|
8627
|
+
key: z.string().describe('Memory key — short, descriptive, no spaces (e.g. "arch_decision", "constraint_auth"). In parallel builds with multiple agents on the same task, prefix with your role/index to avoid collisions (e.g. "builder1_discovered", "builder2_constraint"). Last write to the same key wins.'),
|
|
8174
8628
|
value: z.string().describe('Value to store — plain text, any length'),
|
|
8175
8629
|
},
|
|
8176
8630
|
async ({ taskId, key, value }) => {
|
|
@@ -8249,8 +8703,8 @@ Always call this first — it collapses context from the last 3 steps into one c
|
|
|
8249
8703
|
if (repoPath && task.github?.headBranch) {
|
|
8250
8704
|
try {
|
|
8251
8705
|
const branch = task.github.headBranch
|
|
8252
|
-
const status
|
|
8253
|
-
const logLine =
|
|
8706
|
+
const status = runGit('status --short', repoPath)
|
|
8707
|
+
const logLine = runGit('log --oneline -1', repoPath)
|
|
8254
8708
|
localState = { branch, dirty: status.length > 0, lastCommit: logLine }
|
|
8255
8709
|
} catch { localState = { error: 'Could not read local git state' } }
|
|
8256
8710
|
}
|
package/package.json
CHANGED