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.
Files changed (2) hide show
  1. package/index.js +591 -137
  2. 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 the cursor rules file (.cursor/rules/<task>.mdc) so it doesn't bleed into other tasks
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 commitMsg = `wip(${task?.key?.toLowerCase() || 'task'}): ${(summary || 'parking task').slice(0, 60)}`
1027
- runGit('add -A', repoRoot)
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 rules file (.cursor/rules/<task>.mdc) so Cursor follows task rules
1091
- 6. Posts a comment that you picked up the task
1092
- 7. Notifies the previous developer that you have taken over
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 rules restored${agentRole ? ` (role: ${agentRole})` : ''}. Start coding from where ${task?.parkNote?.parkedBy ? 'the previous developer' : 'you'} left off.`
1249
- : `Branch switch failed — see git.manualSteps. Cursor rules restored.`,
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
- ? `BUILDER role active. Call claim_files to lock your files before editing.`
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
- : null,
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 ? '⚠️ No scout report on this task. Consider running a scout agent first to map the codebase before builders start.' : null,
1541
- !executionPlan.readmePresent ? '⚠️ No implementation plan (README). Builders need a spec to work from.' : null,
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: 'todo',
1569
- priority: task.priority || 'medium',
1570
- taskType: s.role === 'reviewer' ? 'feature' : (task.taskType || 'feature'),
1571
- parentTask: taskId,
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
- createdTasks.push({
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
- Use agentRole="scout" in kickoff_task first, then call scout_task to get your briefing.
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 context)
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
- instruction: 'Please reply in this Cursor chat. I will use your answer to continue. Do NOT proceed until you reply.',
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
- scout: ['scout', 'codebase', 'explore', 'map', 'analyze', 'read'],
2076
- reviewer: ['review', 'security', 'audit', 'test', 'quality'],
2077
- coordinator: ['decompose', 'plan', 'coordinate', 'parallel'],
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
- // Include if skill name matches task type's suggested list
2204
+ // Always include task-type suggested skills
2094
2205
  if (taskTypeSkills.includes(s.name)) return true
2095
- // Include if skill name/description matches role keywords
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 = ['looks good', 'lgtm', 'code is fine', 'no issues', 'all good', 'seems correct']
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 < 20 || genericPhrases.some(g => p.toLowerCase().includes(g))
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
- { check: 'Has at least one approval', pass: pr.approvals > 0,
2410
- note: pr.approvals === 0 ? 'No approvals yet consider running post_pr_review first' : `${pr.approvals} approval(s)` },
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}" will move to Done automatically.`,
2466
- nextStep: `The GitHub webhook will update the board. If the task doesn't move to Done within a minute, call update_task with column="done" manually.`,
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}". Ready to resume coding${agentRole ? ` as ${agentRole}` : ''}.`
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
- ? `BUILDER role active. Call claim_files to lock your files before editing.`
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
- // Determine which group to run next: first group that has at least one
2766
- // subtask not yet reflected in the board subtasks as done.
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
- // Board subtask titles may carry a role prefix like "[BUILDER] Auth hardening..."
2769
- // Match on bare title OR prefixed title
2770
- const isDone = (title) => boardSubtasks.some(s => s.done && (s.title === title || s.title.endsWith(title)))
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.every(t => isDone(t))
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 priorAllDone = execOrder.slice(0, i).every(g => g.every(t => isDone(t)))
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.filter(t => !isDone(t))
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}" for task ${task.key}.`,
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 "${taskId}", type "subagent", name "builder-${i + 1}", summary "Builder ${i + 1} started: ${st.title}"`,
2826
- `2. Call kickoff_task with taskId "${taskId}", agentRole "builder", confirmed true`,
2827
- `3. Call claim_files with taskId "${taskId}" and files [${filesArg}]`,
2828
- `4. Call get_agent_context with taskId "${taskId}" to read the full task plan and suggested skills`,
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 any skills listed in get_agent_context suggestedSkills`,
2833
- `6. Run npm test — all tests must pass before continuing`,
2834
- `7. Call commit_helper with taskId "${taskId}"`,
2835
- `8. Call update_task with taskId "${taskId}" and mark the subtask "${st.title}" as done`,
2836
- `9. Call resume_task with taskId "${taskId}", agentRole "reviewer", confirmed true transition to reviewer role`,
2837
- `10. Call log_session_event with taskId "${taskId}", type "subagent", name "builder-${i + 1}", summary "Builder ${i + 1} finished: ${st.title}"`,
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. Another builder is working on different files at the same time.`,
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 calls kickoff_taskclaim_files → implements → runs tests → commits.`,
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 = dec.executionOrder || []
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
- const doneTitles = targetGroup.filter(t => isDone(t))
2943
- const pendingTitles = targetGroup.filter(t => !isDone(t))
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: true,
2948
- group: groupIndex,
2949
- subtasks: targetGroup,
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 = (finalDec.executionOrder || [])[groupIndex - 1] || []
2979
- const finalBoard = finalTask?.subtasks || []
2980
- const finalIsDone = (t) => finalBoard.some(s => s.done && (s.title === t || s.title.endsWith(t)))
2981
- const stillPending = finalGroup.filter(t => !finalIsDone(t))
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 waiting, or proceed manually.',
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
- nextStep: agentRole === 'scout'
4475
- ? `SCOUT mode. Read the codebase, then save findings with update_task(scoutReport=...). Do NOT modify files.`
4476
- : agentRole === 'coordinator'
4477
- ? `COORDINATOR mode. Read the README, then call decompose_task to split work into parallel/sequential subtasks with file ownership.`
4478
- : agentRole === 'builder'
4479
- ? `BUILDER mode. Call claim_files first, then start coding on "${alreadyHasBranch ? task.github.headBranch : suggestedBranch}".`
4480
- : agentRole === 'reviewer'
4481
- ? `REVIEWER mode. Call review_pr to get the diff, then post_pr_review with your analysis.`
4482
- : recommendedFlow === 'coordinator' && shouldAutoRoute
4483
- ? `⚡ COMPLEX TASK (score ${complexityScore}): Call kickoff_task with agentRole "coordinator" confirmed true → decompose_task → get_parallel_kickoffs → start builders. DO NOT code directly.`
4484
- : recommendedFlow === 'single_builder' && shouldAutoRoute
4485
- ? `🔧 MEDIUM TASK (score ${complexityScore}): Call kickoff_task with agentRole "builder" confirmed true → claim_files → implement → commit → raise_pr.`
4486
- : alreadyHasBranch
4487
- ? `SIMPLE TASK (score ${complexityScore}). Branch "${task.github.headBranch}" existscall claim_files and start coding.`
4488
- : `SIMPLE TASK (score ${complexityScore}). Call create_branch to create "${suggestedBranch}", then claim_files and code.`,
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
- 'Approve or reject a specific approval request by approvalId. Only the designated reviewer can call this.',
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
- call(() => api.post(`/api/tasks/${taskId}/approvals/${approvalId}/decide`, { decision, note }))
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
- return { error: true, cause: e._cause || 'unknown', message: e.message?.split('\n')[0] }
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
- const previewBranch = `${isFix2 ? 'fix' : 'feature'}/${task.key.toLowerCase()}-${slug2}`
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 task-specific cursor rules file — coding is done
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
- cursorRulesCleared: deletedRulesFile
7026
- ? { cleared: true, path: deletedRulesFile, note: 'Task-specific Cursor rules file deleted — coding is complete.' }
7027
- : { cleared: false },
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
- : 'PR is live. The GitHub webhook will move the task to in_review and notify the reviewer within seconds.',
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: `Request review on PR #${branch.prNumber}`,
7766
- why: `Your PR is already open. After pushing the rebase, request reviewers so they can approve and merge.`,
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: 'Request review via GitHub CLI', cmd: `gh pr review ${branch.prNumber} --request-changes` },
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 = execSync(`git -C "${repoPath}" status --short 2>/dev/null`, { encoding: 'utf8' }).trim()
8253
- const logLine = execSync(`git -C "${repoPath}" log --oneline -1 2>/dev/null`, { encoding: 'utf8' }).trim()
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "internaltool-mcp",
3
- "version": "1.6.45",
3
+ "version": "1.6.52",
4
4
  "description": "MCP server for InternalTool — connect AI assistants (Claude Code, Cursor) to your project and task management platform",
5
5
  "type": "module",
6
6
  "main": "index.js",