internaltool-mcp 1.6.22 → 1.6.24

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 +650 -52
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -434,8 +434,10 @@ Set confirmed=false first to read everything, then confirmed=true to execute.`,
434
434
  confirmed: z.boolean().optional().default(false).describe('Set true to execute after reviewing the park note'),
435
435
  repoPath: z.string().optional().describe('Absolute path to the local git repo (defaults to MCP process working directory).'),
436
436
  autoStash: z.boolean().optional().default(false).describe('Auto-stash dirty working tree before switching branches instead of blocking. Stash is labelled with the task key for easy recovery.'),
437
+ agentRole: z.enum(['builder', 'reviewer', 'scout', 'coordinator']).optional()
438
+ .describe('Set the agent role for this task session. Role-specific behavioral constraints are injected into cursor rules.'),
437
439
  },
438
- async ({ taskId, confirmed = false, repoPath, autoStash = false }) => {
440
+ async ({ taskId, confirmed = false, repoPath, autoStash = false, agentRole }) => {
439
441
  const taskRes = await api.get(`/api/tasks/${taskId}`)
440
442
  const task = taskRes?.data?.task
441
443
  const branch = task?.github?.headBranch || null
@@ -546,10 +548,21 @@ Set confirmed=false first to read everything, then confirmed=true to execute.`,
546
548
  // ── Clear park note + server-side comment & notification ──────────────
547
549
  await api.patch(`/api/tasks/${taskId}/unpark`, {})
548
550
 
549
- // ── Restore cursor rules file ──────────────────────────────────────────
551
+ // ── Restore cursor rules file (with role injection if set) ───────────────
550
552
  let cursorRulesFile = null
551
- if (task?.cursorRules?.trim()) {
552
- cursorRulesFile = writeCursorRulesFile(task.key, task.cursorRules, repoPath)
553
+ const hasCursorRulesUnpark = task?.cursorRules?.trim()
554
+ if (hasCursorRulesUnpark || agentRole) {
555
+ cursorRulesFile = writeCursorRulesFile(
556
+ task.key,
557
+ hasCursorRulesUnpark || '(No task-specific rules — follow role constraints above.)',
558
+ repoPath,
559
+ agentRole || null
560
+ )
561
+ }
562
+
563
+ // Persist agentRole to server if set
564
+ if (agentRole) {
565
+ api.patch(`/api/tasks/${taskId}`, { agentRole }).catch(() => {/* non-fatal */})
553
566
  }
554
567
 
555
568
  // ── #9 Last commit metadata for handoff context ───────────────────────
@@ -557,17 +570,364 @@ Set confirmed=false first to read everything, then confirmed=true to execute.`,
557
570
 
558
571
  return text({
559
572
  unparked: true,
560
- task: { key: task?.key, title: task?.title },
573
+ task: { key: task?.key, title: task?.title, agentRole: agentRole || null },
561
574
  git: gitResult || { switched: false, reason: 'No branch linked or repo not found' },
562
575
  autoStashed: autoStash ? 'Changes were auto-stashed before branch switch. Run: git stash list to see them.' : null,
563
576
  lastCommit: lastCommit || null,
564
- cursorRules: cursorRulesFile ? { restored: true, path: cursorRulesFile } : { restored: false },
577
+ cursorRules: cursorRulesFile
578
+ ? { restored: true, path: cursorRulesFile, agentRole: agentRole || null }
579
+ : { restored: false },
565
580
  commentPosted: true,
566
581
  previousDevNotified: true,
567
582
  parkNote: task?.parkNote || null,
568
583
  message: gitResult?.switched
569
- ? `You are now on branch "${branch}". Cursor rules restored. Start coding from where ${task?.parkNote?.parkedBy ? 'the previous developer' : 'you'} left off.`
584
+ ? `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.`
570
585
  : `Branch switch failed — see git.manualSteps. Cursor rules restored.`,
586
+ nextStep: agentRole === 'builder'
587
+ ? `BUILDER role active. Call claim_files to lock your files before editing.`
588
+ : agentRole === 'scout'
589
+ ? `SCOUT role active. Read-only mode — map the codebase and save findings with update_task(scoutReport=...).`
590
+ : agentRole === 'coordinator'
591
+ ? `COORDINATOR role active. Call decompose_task to plan parallel workstreams.`
592
+ : agentRole === 'reviewer'
593
+ ? `REVIEWER role active. Call review_pr to start the review chain.`
594
+ : null,
595
+ })
596
+ }
597
+ )
598
+
599
+ // ── claim_files ──────────────────────────────────────────────────────────────
600
+ server.tool(
601
+ 'claim_files',
602
+ `Claim exclusive ownership of files for this task session.
603
+
604
+ Prevents merge conflicts by ensuring no two in-progress tasks edit the same file.
605
+ Call this at the start of a builder session before editing any files.
606
+
607
+ Returns a conflict list if any claimed files are already owned by another in-progress task.
608
+ Ownership is automatically released when the task is parked.
609
+
610
+ Roles: BUILDER agents MUST call this before editing. Coordinators call this as part of decompose_task.`,
611
+ {
612
+ taskId: z.string().describe("Task's MongoDB ObjectId"),
613
+ files: z.array(z.string()).min(1).describe('List of file paths this task will exclusively edit (e.g. ["server/routes/tasks.js", "client/src/App.jsx"])'),
614
+ },
615
+ async ({ taskId, files }) => {
616
+ const res = await api.post(`/api/tasks/${taskId}/files/claim`, { files })
617
+ if (!res?.success) {
618
+ if (res?.conflicts) {
619
+ return text({
620
+ blocked: true,
621
+ reason: 'File ownership conflict — these files are already claimed by another in-progress task.',
622
+ conflicts: res.conflicts,
623
+ message: 'Wait for the conflicting task to release the files, or coordinate with the other developer to resolve the overlap.',
624
+ })
625
+ }
626
+ return errorText(res?.message || 'Could not claim files')
627
+ }
628
+ return text({
629
+ claimed: true,
630
+ files,
631
+ taskId,
632
+ message: `${files.length} file(s) claimed. No other in-progress task in this project may edit these files until you release or park.`,
633
+ nextStep: 'You may now safely edit the claimed files. Files are auto-released when you call park_task.',
634
+ })
635
+ }
636
+ )
637
+
638
+ // ── release_files ─────────────────────────────────────────────────────────────
639
+ server.tool(
640
+ 'release_files',
641
+ `Release all file ownership claims for a task.
642
+
643
+ Call this when a builder subtask is complete and another task needs to edit the same files.
644
+ Files are also released automatically when the task is parked.`,
645
+ {
646
+ taskId: z.string().describe("Task's MongoDB ObjectId"),
647
+ },
648
+ async ({ taskId }) => {
649
+ const res = await api.post(`/api/tasks/${taskId}/files/release`, {})
650
+ if (!res?.success) return errorText(res?.message || 'Could not release files')
651
+ return text({
652
+ released: true,
653
+ taskId,
654
+ message: 'All file claims released. Other tasks may now claim these files.',
655
+ })
656
+ }
657
+ )
658
+
659
+ // ── decompose_task ────────────────────────────────────────────────────────────
660
+ server.tool(
661
+ 'decompose_task',
662
+ `Coordinator tool: decompose an implementation plan into parallel subtasks with file ownership and role assignments.
663
+
664
+ Use this when you are acting as a COORDINATOR agent and need to break a large task into
665
+ parallel workstreams that multiple builder agents can execute without stepping on each other.
666
+
667
+ What this does:
668
+ 1. Reads the task's implementation plan (README)
669
+ 2. Analyzes the plan to identify independent work units
670
+ 3. Returns a structured execution plan with:
671
+ - Parallel subtask groups (tasks that can run simultaneously)
672
+ - File ownership per subtask (no overlaps — each file appears in exactly one subtask)
673
+ - Role assignment per subtask (builder/scout/reviewer)
674
+ - Dependency order (which subtasks must complete before others start)
675
+ 4. Saves the decomposition plan to the task (decomposition field)
676
+ 5. Optionally creates the subtasks on the board
677
+
678
+ REQUIRED BEFORE BUILDERS START:
679
+ - A scout report should exist (call kickoff_task with agentRole=scout first)
680
+ - The README must describe what needs to be built
681
+
682
+ Call confirmed=false to preview the decomposition, confirmed=true to save it.`,
683
+ {
684
+ taskId: z.string().describe("Task's MongoDB ObjectId"),
685
+ subtaskPlan: z.array(z.object({
686
+ title: z.string().describe('Subtask title'),
687
+ role: z.enum(['builder', 'scout', 'reviewer']).describe('Agent role for this subtask'),
688
+ files: z.array(z.string()).describe('Files this subtask exclusively owns (no overlap with other subtasks)'),
689
+ description: z.string().describe('What this subtask implements'),
690
+ dependsOn: z.array(z.string()).optional().describe('Titles of subtasks that must complete before this one starts'),
691
+ parallel: z.boolean().optional().default(true).describe('Can this subtask run in parallel with others at the same level?'),
692
+ })).min(1).describe('Decomposed subtask execution plan. File ownership must not overlap between subtasks.'),
693
+ confirmed: z.boolean().optional().default(false).describe('Set true to save the decomposition and create subtasks on the board'),
694
+ },
695
+ async ({ taskId, subtaskPlan, confirmed = false }) => {
696
+ const taskRes = await apiWithRetry(() => api.get(`/api/tasks/${taskId}`))
697
+ if (!taskRes?.success) return errorText('Task not found')
698
+ const task = taskRes.data.task
699
+
700
+ // Validate: no file overlap between subtasks
701
+ const fileOwnershipMap = {}
702
+ const conflicts = []
703
+ for (const subtask of subtaskPlan) {
704
+ for (const file of (subtask.files || [])) {
705
+ if (fileOwnershipMap[file]) {
706
+ conflicts.push({ file, claimedBy: [fileOwnershipMap[file], subtask.title] })
707
+ } else {
708
+ fileOwnershipMap[file] = subtask.title
709
+ }
710
+ }
711
+ }
712
+
713
+ if (conflicts.length > 0) {
714
+ return text({
715
+ blocked: true,
716
+ reason: 'File ownership overlap detected — each file must appear in exactly one subtask.',
717
+ conflicts,
718
+ message: 'Fix the overlapping file assignments before confirming the decomposition.',
719
+ })
720
+ }
721
+
722
+ // Group subtasks by dependency level for parallel execution visualization
723
+ const parallelGroups = []
724
+ const placed = new Set()
725
+ let safetyLimit = subtaskPlan.length + 1
726
+ while (placed.size < subtaskPlan.length && safetyLimit-- > 0) {
727
+ const batch = subtaskPlan.filter(s =>
728
+ !placed.has(s.title) &&
729
+ (s.dependsOn || []).every(dep => placed.has(dep))
730
+ )
731
+ if (!batch.length) break
732
+ parallelGroups.push(batch.map(s => s.title))
733
+ batch.forEach(s => placed.add(s.title))
734
+ }
735
+
736
+ const executionPlan = {
737
+ taskKey: task.key,
738
+ taskTitle: task.title,
739
+ totalSubtasks: subtaskPlan.length,
740
+ subtasks: subtaskPlan.map(s => ({
741
+ title: s.title,
742
+ role: s.role,
743
+ files: s.files,
744
+ description: s.description,
745
+ dependsOn: s.dependsOn || [],
746
+ parallel: s.parallel !== false,
747
+ })),
748
+ executionOrder: parallelGroups,
749
+ fileOwnershipMap,
750
+ scoutReportPresent: !!(task.scoutReport?.trim()),
751
+ readmePresent: !!(task.readmeMarkdown?.trim()),
752
+ }
753
+
754
+ if (!confirmed) {
755
+ return text({
756
+ decomposition: executionPlan,
757
+ warnings: [
758
+ !executionPlan.scoutReportPresent ? '⚠️ No scout report on this task. Consider running a scout agent first to map the codebase before builders start.' : null,
759
+ !executionPlan.readmePresent ? '⚠️ No implementation plan (README). Builders need a spec to work from.' : null,
760
+ ].filter(Boolean),
761
+ requiresConfirmation: true,
762
+ message: 'Review the decomposition above. Call decompose_task again with confirmed=true to save it and create the subtasks on the board.',
763
+ })
764
+ }
765
+
766
+ // Save decomposition to task
767
+ const decompositionJson = JSON.stringify(executionPlan, null, 2)
768
+ try {
769
+ await api.patch(`/api/tasks/${taskId}`, { decomposition: decompositionJson })
770
+ } catch { /* non-fatal — decomposition is returned regardless */ }
771
+
772
+ // Create subtasks on the board
773
+ const currentSubtasks = task.subtasks || []
774
+ const newSubtasks = [
775
+ ...currentSubtasks,
776
+ ...subtaskPlan.map((s, i) => ({
777
+ title: `[${s.role.toUpperCase()}] ${s.title}`,
778
+ done: false,
779
+ order: currentSubtasks.length + i,
780
+ })),
781
+ ]
782
+ try {
783
+ await api.patch(`/api/tasks/${taskId}`, { subtasks: newSubtasks })
784
+ } catch { /* non-fatal */ }
785
+
786
+ return text({
787
+ decomposed: true,
788
+ taskKey: task.key,
789
+ executionPlan,
790
+ subtasksCreated: subtaskPlan.length,
791
+ message: `Decomposition saved. ${subtaskPlan.length} subtask(s) added to the board.`,
792
+ nextStep: parallelGroups.length > 0
793
+ ? `Start with parallel group 1: ${parallelGroups[0].join(', ')}. Each builder agent should call kickoff_task with agentRole="builder" and claim_files before coding.`
794
+ : 'Start the subtasks in order. Each builder should call kickoff_task with agentRole="builder" and claim_files.',
795
+ })
796
+ }
797
+ )
798
+
799
+ // ── scout_task ────────────────────────────────────────────────────────────────
800
+ server.tool(
801
+ 'scout_task',
802
+ `Scout agent entry point: analyze the codebase for a task and save a structured scout report.
803
+
804
+ Call this BEFORE builders start when the Coordinator needs a codebase map.
805
+ Use agentRole="scout" in kickoff_task first, then call scout_task to get your briefing.
806
+
807
+ Two-phase flow:
808
+ Phase 1 — confirmed=false: get the briefing (what to analyze, report format)
809
+ Phase 2 — confirmed=true + report: save your findings to the task
810
+
811
+ The scout report is consumed by:
812
+ - decompose_task (warns if missing)
813
+ - get_agent_context (included in builder context)
814
+ - kickoff_task (surfaced as part of builder brief)
815
+
816
+ Scouts MUST NOT modify any source code files or create branches.`,
817
+ {
818
+ taskId: z.string().describe("Task's MongoDB ObjectId"),
819
+ confirmed: z.boolean().optional().default(false).describe('Set true to save the report. Set false (default) to get the briefing on what to analyze.'),
820
+ report: z.string().optional().default('').describe('Your structured scout findings in markdown (required when confirmed=true)'),
821
+ },
822
+ async ({ taskId, confirmed = false, report = '' }) => {
823
+ const taskRes = await apiWithRetry(() => api.get(`/api/tasks/${taskId}`))
824
+ if (!taskRes?.success) return errorText('Task not found')
825
+ const task = taskRes.data.task
826
+
827
+ if (!confirmed) {
828
+ return text({
829
+ brief: {
830
+ key: task.key,
831
+ title: task.title,
832
+ implementationPlan: task.readmeMarkdown || '(no README — ask coordinator to write one first)',
833
+ },
834
+ currentScoutReport: task.scoutReport?.trim() || null,
835
+ scoutInstructions: {
836
+ objective: 'Map the codebase as it relates to this task. Identify every file that will need to change.',
837
+ analyzeThese: [
838
+ 'Entry points — where does execution start for this feature area?',
839
+ 'File dependency graph — which files import from which?',
840
+ 'Existing patterns — what conventions does this codebase use (naming, folder structure, abstractions)?',
841
+ 'Conflict risks — are any in-progress tasks touching the same files?',
842
+ 'Complexity estimate — lines of code, number of files, coupling, test coverage',
843
+ ],
844
+ reportFormat: {
845
+ summary: 'One paragraph overview of what this task touches in the codebase',
846
+ entryPoints: ['file/path/entry.ts', '...'],
847
+ keyFiles: [{ path: 'file/path.ts', role: 'what it does for this task' }],
848
+ risks: ['Potential gotchas, breaking changes, or hidden complexity'],
849
+ suggestedFileOwnership: { 'Subtask Name': ['file1.ts', 'file2.ts'] },
850
+ complexityEstimate: 'low | medium | high',
851
+ },
852
+ },
853
+ requiresConfirmation: true,
854
+ message: `Analyze the codebase for ${task.key}, then call scout_task again with confirmed=true and your report string.`,
855
+ })
856
+ }
857
+
858
+ if (!report.trim()) return errorText('report is required when confirmed=true. Pass your scout findings as a markdown string.')
859
+
860
+ try {
861
+ await api.patch(`/api/tasks/${taskId}`, { scoutReport: report })
862
+ } catch (e) {
863
+ return errorText(`Failed to save scout report: ${e?.message || 'unknown error'}`)
864
+ }
865
+
866
+ return text({
867
+ saved: true,
868
+ taskKey: task.key,
869
+ message: `Scout report saved for ${task.key}.`,
870
+ nextStep: 'The coordinator can now call decompose_task — the scout report will be included in builder context automatically via get_agent_context.',
871
+ })
872
+ }
873
+ )
874
+
875
+ // ── get_agent_context ─────────────────────────────────────────────────────────
876
+ server.tool(
877
+ 'get_agent_context',
878
+ `Get a complete context package for starting work on a task as a specific agent role.
879
+
880
+ Call this instead of get_task when starting a session — it returns everything composed and
881
+ ready to use: systemPrompt, role behavioral constraints, implementation plan, cursor rules,
882
+ scout report, decomposition plan, claimed files, and project-level custom guidance.
883
+
884
+ Use this as the single entry point for any agent picking up a task. Replaces the need to
885
+ call get_task + kickoff_task preview separately.
886
+
887
+ Returns systemPrompt ready to use as a Claude system prompt.`,
888
+ {
889
+ taskId: z.string().describe("Task's MongoDB ObjectId"),
890
+ role: z.enum(['builder', 'reviewer', 'scout', 'coordinator']).optional()
891
+ .describe('Agent role for this session. Falls back to task.agentRole if omitted.'),
892
+ },
893
+ async ({ taskId, role }) => {
894
+ const qs = role ? `?role=${role}` : ''
895
+ const res = await apiWithRetry(() => api.get(`/api/tasks/${taskId}/agent-context${qs}`))
896
+ if (!res?.success) return errorText(res?.message || 'Failed to get agent context')
897
+ const ctx = res.data
898
+
899
+ const effectiveRole = ctx.role
900
+ const roleRules = effectiveRole && ROLE_RULES[effectiveRole] ? ROLE_RULES[effectiveRole] : null
901
+
902
+ // Compose the full system prompt
903
+ const parts = []
904
+ if (effectiveRole) {
905
+ const roleLabel = ctx.customRoleLabel || effectiveRole.toUpperCase()
906
+ parts.push(`You are a ${roleLabel} agent working on task ${ctx.taskKey}: "${ctx.taskTitle}".`)
907
+ }
908
+ if (roleRules) parts.push(roleRules)
909
+ if (ctx.customPromptHint) parts.push(`\n## Project-Specific Guidance\n\n${ctx.customPromptHint}`)
910
+ if (ctx.task?.cursorRules?.trim()) parts.push(`\n## Task-Specific Cursor Rules\n\n${ctx.task.cursorRules}`)
911
+ if (ctx.task?.readmeMarkdown?.trim()) parts.push(`\n## Implementation Plan\n\n${ctx.task.readmeMarkdown}`)
912
+ if (ctx.task?.scoutReport?.trim()) parts.push(`\n## Scout Report (Codebase Analysis)\n\n${ctx.task.scoutReport}`)
913
+ if (ctx.task?.decomposition?.trim()) {
914
+ try {
915
+ const dec = JSON.parse(ctx.task.decomposition)
916
+ parts.push(`\n## Execution Plan\n\nExecution order: ${JSON.stringify(dec.executionOrder)}\nFile ownership: ${JSON.stringify(dec.fileOwnershipMap, null, 2)}`)
917
+ } catch { /* non-fatal */ }
918
+ }
919
+
920
+ return text({
921
+ role: effectiveRole,
922
+ taskKey: ctx.taskKey,
923
+ taskTitle: ctx.taskTitle,
924
+ systemPrompt: parts.join('\n\n'),
925
+ allowedTools: ctx.customAllowedTools || null,
926
+ claimedFiles: ctx.task?.claimedFiles || [],
927
+ warnings: ctx.warnings || [],
928
+ task: ctx.task,
929
+ project: ctx.project,
930
+ usage: 'Use systemPrompt as your Claude system prompt. If allowedTools is set, restrict your MCP tool calls to that list.',
571
931
  })
572
932
  }
573
933
  )
@@ -579,7 +939,17 @@ Set confirmed=false first to read everything, then confirmed=true to execute.`,
579
939
 
580
940
  Returns the diff file-by-file (filename, additions, deletions, patch hunks), the task's README spec, acceptance criteria, and CI check results so you can verify code against the plan.
581
941
 
582
- Call this FIRST before post_pr_review. Use it when the developer asks "review PR", "check the PR for TASK-X", or "is this PR ready to merge".`,
942
+ MANDATORY: After calling this tool, you MUST:
943
+ 1. Read every file in diff.files — check the patch hunk line by line
944
+ 2. Compare each change against spec.implementationPlan — does the code implement what was planned?
945
+ 3. Identify what is present, what is missing, what is wrong
946
+ 4. Build analysisPoints (min 2) referencing specific filenames and function names
947
+ 5. Only THEN call post_pr_review with your verdict and analysisPoints
948
+
949
+ Do NOT call post_pr_review immediately after this without analyzing the diff first.
950
+ Do NOT approve a PR without reading the patch hunks.
951
+
952
+ Use this when the developer asks "review PR", "check the PR for TASK-X", or "is this PR ready to merge".`,
583
953
  {
584
954
  taskId: z.string().describe("Task's MongoDB ObjectId (used to find the project + PR number)"),
585
955
  },
@@ -654,48 +1024,75 @@ Call this FIRST before post_pr_review. Use it when the developer asks "review PR
654
1024
  // ── post_pr_review ────────────────────────────────────────────────────────────
655
1025
  server.tool(
656
1026
  'post_pr_review',
657
- `Post a GitHub PR review after analyzing the code diff.
1027
+ `Post a GitHub PR review after analyzing the code diff from review_pr.
658
1028
 
659
1029
  Supports three actions:
660
1030
  - APPROVE — Approves the PR. Notifies the developer they can merge.
661
1031
  - REQUEST_CHANGES — Blocks merge. Developer gets notified. Task board updated.
662
1032
  - COMMENT — Leaves a comment without approving or blocking.
663
1033
 
664
- Always call review_pr first to get the diff and spec context. Then call this with your verdict and reasoning.
1034
+ REQUIRED WORKFLOW — you MUST call review_pr first and read the diff before calling this.
1035
+ You MUST populate analysisPoints with specific findings from the diff — what you checked,
1036
+ what matched the spec, what is missing or broken. Generic analysis like "code looks good"
1037
+ will be rejected. Be file-specific: reference filenames, function names, line numbers.
665
1038
 
666
- IMPORTANT: This posts a real GitHub review and updates the InternalTool board. The human must confirm before this runs — always show the review body to the user and ask for approval before calling with confirmed=true.`,
1039
+ Human approval gate: confirmed=false shows preview, confirmed=true posts to GitHub.
1040
+
1041
+ Returns reviewId — save it and pass it to merge_pr to prove semantic review happened.`,
667
1042
  {
668
- taskId: z.string().describe("Task's MongoDB ObjectId"),
669
- event: z.enum(['APPROVE', 'REQUEST_CHANGES', 'COMMENT']).describe('Review action'),
670
- body: z.string().describe('Review summary — be specific: what was good, what needs fixing, which files/lines'),
671
- confirmed: z.boolean().optional().default(false).describe('Set true only after the human has read and approved the review text'),
1043
+ taskId: z.string().describe("Task's MongoDB ObjectId"),
1044
+ event: z.enum(['APPROVE', 'REQUEST_CHANGES', 'COMMENT']).describe('Review action'),
1045
+ body: z.string().describe('Review summary posted to GitHub — be specific about files, functions, issues'),
1046
+ analysisPoints: z.array(z.string()).min(2).describe(
1047
+ 'Structured findings from your diff analysis. Each entry must reference a specific file, function, or line. ' +
1048
+ 'Examples: ["calculator.js: add/subtract/multiply all present and correct", ' +
1049
+ '"divide() function missing — spec requires divide-by-zero handling", ' +
1050
+ '"No input validation on any function — null/string inputs silently coerce"]. ' +
1051
+ 'Minimum 2 points required. Generic entries like "code looks fine" are rejected.'
1052
+ ),
1053
+ confirmed: z.boolean().optional().default(false).describe('Set true after the human has read and approved the review text'),
672
1054
  },
673
- async ({ taskId, event, body: reviewBody, confirmed = false }) => {
1055
+ async ({ taskId, event, body: reviewBody, analysisPoints, confirmed = false }) => {
674
1056
  const taskRes = await apiWithRetry(() => api.get(`/api/tasks/${taskId}`))
675
1057
  if (!taskRes?.success) return errorText('Task not found')
676
1058
  const task = taskRes.data.task
677
1059
 
678
1060
  const prNumber = task.github?.prNumber
679
1061
  const projectId = task.project?._id || task.project
680
- if (!prNumber) return errorText('No PR linked to this task.')
1062
+ if (!prNumber) return errorText('No PR linked to this task. Call raise_pr first.')
681
1063
 
682
- // Always preview firsthuman must confirm
1064
+ // Enforce that analysisPoints are specific reject obviously generic ones
1065
+ const genericPhrases = ['looks good', 'lgtm', 'code is fine', 'no issues', 'all good', 'seems correct']
1066
+ const tooGeneric = analysisPoints.filter(p =>
1067
+ p.trim().length < 20 || genericPhrases.some(g => p.toLowerCase().includes(g))
1068
+ )
1069
+ if (tooGeneric.length > 0) {
1070
+ return text({
1071
+ blocked: true,
1072
+ reason: 'analysisPoints must be file-specific and reference actual diff content.',
1073
+ rejected: tooGeneric,
1074
+ instruction: 'Call review_pr first to get the diff, then re-examine each file and describe specific findings.',
1075
+ })
1076
+ }
1077
+
1078
+ // Preview — human must confirm
683
1079
  if (!confirmed) {
684
1080
  return text({
685
1081
  preview: {
686
- action: event,
1082
+ action: event,
687
1083
  prNumber,
688
- taskKey: task.key,
689
- taskTitle: task.title,
1084
+ taskKey: task.key,
1085
+ taskTitle: task.title,
690
1086
  reviewBody,
1087
+ analysisPoints,
691
1088
  },
692
1089
  warning: event === 'APPROVE'
693
1090
  ? '✅ This will APPROVE the PR on GitHub and notify the developer they can merge.'
694
1091
  : event === 'REQUEST_CHANGES'
695
- ? '⚠️ This will REQUEST CHANGES — the developer will be blocked from merging until they fix the issues and you re-approve.'
1092
+ ? '⚠️ This will REQUEST CHANGES — the developer will be blocked from merging until they fix the issues.'
696
1093
  : 'ℹ️ This will post a comment on the PR without approving or blocking.',
697
1094
  requiresConfirmation: true,
698
- message: `Show the review body above to the user and ask them to confirm. Then call post_pr_review again with confirmed=true.`,
1095
+ message: 'Show the review body and analysis points to the user and ask them to confirm. Then call post_pr_review again with confirmed=true.',
699
1096
  })
700
1097
  }
701
1098
 
@@ -705,20 +1102,24 @@ IMPORTANT: This posts a real GitHub review and updates the InternalTool board. T
705
1102
  )
706
1103
  if (!res?.success) return errorText(res?.message || 'Could not post review')
707
1104
 
1105
+ const reviewId = res.data?.reviewId
1106
+
708
1107
  return text({
709
- posted: true,
1108
+ posted: true,
710
1109
  event,
711
1110
  prNumber,
712
- taskKey: task.key,
713
- message: event === 'APPROVE'
1111
+ reviewId,
1112
+ taskKey: task.key,
1113
+ analysisPoints,
1114
+ message: event === 'APPROVE'
714
1115
  ? `✅ PR #${prNumber} approved. Developer has been notified and can now merge.`
715
1116
  : event === 'REQUEST_CHANGES'
716
1117
  ? `⚠️ Changes requested on PR #${prNumber}. Developer has been notified. Task board updated.`
717
1118
  : `💬 Comment posted on PR #${prNumber}.`,
718
1119
  nextStep: event === 'APPROVE'
719
- ? `Call merge_pr with taskId="${taskId}" to merge, or wait for the developer to merge.`
1120
+ ? `Call merge_pr with taskId="${taskId}" and reviewId="${reviewId}" to merge.`
720
1121
  : event === 'REQUEST_CHANGES'
721
- ? `Wait for the developer to push fixes. They'll call fix_pr_feedback, then re-push. You'll get notified.`
1122
+ ? `Wait for the developer to push fixes. They'll call fix_pr_feedback, then re-push. You'll be notified.`
722
1123
  : null,
723
1124
  })
724
1125
  }
@@ -727,28 +1128,35 @@ IMPORTANT: This posts a real GitHub review and updates the InternalTool board. T
727
1128
  // ── merge_pr ─────────────────────────────────────────────────────────────────
728
1129
  server.tool(
729
1130
  'merge_pr',
730
- `Merge a pull request after human approval. Runs safety checks before executing.
1131
+ `Merge a pull request after semantic review and human approval.
1132
+
1133
+ REQUIRED WORKFLOW:
1134
+ 1. Call review_pr — read the diff and spec
1135
+ 2. Call post_pr_review — post your verdict with specific analysisPoints, get back reviewId
1136
+ 3. Call merge_pr — pass reviewId to prove semantic review happened
1137
+
1138
+ If the PR already has GitHub approvals from before this session, reviewId is not required.
731
1139
 
732
1140
  Safety checks (pre-merge):
1141
+ - Semantic review: reviewId required if PR has no existing GitHub approvals
733
1142
  - PR must be open and not already merged
734
1143
  - No pending change requests
735
1144
  - CI checks must be passing (pass skipChecks=true to override)
736
1145
 
737
1146
  Human approval gate:
738
- - confirmed=false (default): shows safety check results and asks for confirmation
1147
+ - confirmed=false (default): shows all checks and asks for confirmation
739
1148
  - confirmed=true: executes the merge
740
1149
 
741
- The task moves to Done automatically via the existing GitHub webhook. A final comment is posted on the task.
742
-
743
- Use this when the reviewer says "merge it", "looks good, ship it", or "merge the PR for TASK-X".`,
1150
+ The task moves to Done automatically via the GitHub webhook.`,
744
1151
  {
745
1152
  taskId: z.string().describe("Task's MongoDB ObjectId"),
746
1153
  confirmed: z.boolean().optional().default(false).describe('Set true only after the human has reviewed the safety checks and approved the merge'),
747
1154
  mergeMethod: z.enum(['squash', 'merge', 'rebase']).optional().default('squash').describe('Merge strategy (default: squash)'),
748
1155
  commitTitle: z.string().optional().describe('Override the merge commit title. Defaults to PR title (#number).'),
749
1156
  skipChecks: z.boolean().optional().default(false).describe('Skip CI check gate — use only when checks are informational or flaky'),
1157
+ reviewId: z.number().optional().describe('GitHub review ID returned by post_pr_review. Required when PR has no existing approvals — proves semantic review happened in this session.'),
750
1158
  },
751
- async ({ taskId, confirmed = false, mergeMethod = 'squash', commitTitle, skipChecks = false }) => {
1159
+ async ({ taskId, confirmed = false, mergeMethod = 'squash', commitTitle, skipChecks = false, reviewId }) => {
752
1160
  const taskRes = await apiWithRetry(() => api.get(`/api/tasks/${taskId}`))
753
1161
  if (!taskRes?.success) return errorText('Task not found')
754
1162
  const task = taskRes.data.task
@@ -769,8 +1177,30 @@ Use this when the reviewer says "merge it", "looks good, ship it", or "merge the
769
1177
  return text({ blocked: true, reason: `PR #${prNumber} is ${pr.state} — cannot merge a closed PR.` })
770
1178
  }
771
1179
 
1180
+ // ── Semantic review gate ──────────────────────────────────────────────────
1181
+ // If the PR has no GitHub approvals from any session, a reviewId from
1182
+ // post_pr_review in this session is mandatory. This ensures code was
1183
+ // actually read and analyzed before merge, not just rubber-stamped.
1184
+ const hasExistingApproval = pr.approvals > 0
1185
+ const hasSessionReview = !!reviewId
1186
+ if (!hasExistingApproval && !hasSessionReview) {
1187
+ return text({
1188
+ blocked: true,
1189
+ reason: 'No semantic review has been performed on this PR.',
1190
+ required: [
1191
+ `1. Call review_pr with taskId="${taskId}" to read the diff and spec`,
1192
+ `2. Analyze the diff — check every file against the implementation plan`,
1193
+ `3. Call post_pr_review with taskId="${taskId}", your verdict, and specific analysisPoints`,
1194
+ `4. Pass the returned reviewId here to prove the review happened`,
1195
+ ],
1196
+ note: 'If this PR was already approved by a reviewer on GitHub, merge_pr will allow it through automatically.',
1197
+ })
1198
+ }
1199
+
772
1200
  // Build safety check summary
773
1201
  const safetyChecks = [
1202
+ { check: 'Semantic review performed', pass: hasExistingApproval || hasSessionReview,
1203
+ note: hasSessionReview ? `reviewId ${reviewId} from this session` : hasExistingApproval ? `${pr.approvals} existing GitHub approval(s)` : 'Missing — call review_pr then post_pr_review' },
774
1204
  { check: 'PR is open', pass: pr.state === 'open' },
775
1205
  { check: 'Not already merged', pass: !pr.merged },
776
1206
  { check: 'No change requests', pass: pr.changesRequested === 0,
@@ -858,8 +1288,10 @@ Flow:
858
1288
  confirmed: z.boolean().optional().default(false).describe('Set true after reviewing the context to execute the resume'),
859
1289
  repoPath: z.string().optional().describe('Absolute path to the local git repo. Defaults to MCP process working directory.'),
860
1290
  autoStash: z.boolean().optional().default(false).describe('If working tree is dirty, auto-stash before switching. Labelled with task key.'),
1291
+ agentRole: z.enum(['builder', 'reviewer', 'scout', 'coordinator']).optional()
1292
+ .describe('Set the agent role for this resumed session. Role-specific constraints are injected into cursor rules.'),
861
1293
  },
862
- async ({ taskId, confirmed = false, repoPath, autoStash = false }) => {
1294
+ async ({ taskId, confirmed = false, repoPath, autoStash = false, agentRole }) => {
863
1295
  // ── Fetch task ────────────────────────────────────────────────────────
864
1296
  let taskRes
865
1297
  try { taskRes = await apiWithRetry(() => api.get(`/api/tasks/${taskId}`)) }
@@ -1017,10 +1449,21 @@ Flow:
1017
1449
  }
1018
1450
  }
1019
1451
 
1020
- // Restore cursor rules
1452
+ // Restore cursor rules (with role injection if set)
1021
1453
  let cursorRulesFile = null
1022
- if (task?.cursorRules?.trim() && repoRoot) {
1023
- cursorRulesFile = writeCursorRulesFile(task.key, task.cursorRules, repoPath)
1454
+ const hasCursorRulesResume = task?.cursorRules?.trim()
1455
+ if ((hasCursorRulesResume || agentRole) && repoRoot) {
1456
+ cursorRulesFile = writeCursorRulesFile(
1457
+ task.key,
1458
+ hasCursorRulesResume || '(No task-specific rules — follow role constraints above.)',
1459
+ repoPath,
1460
+ agentRole || null
1461
+ )
1462
+ }
1463
+
1464
+ // Persist agentRole to server if set
1465
+ if (agentRole) {
1466
+ api.patch(`/api/tasks/${taskId}`, { agentRole }).catch(() => {/* non-fatal */})
1024
1467
  }
1025
1468
 
1026
1469
  // Last commit metadata
@@ -1028,18 +1471,28 @@ Flow:
1028
1471
 
1029
1472
  return text({
1030
1473
  resumed: true,
1031
- task: { key: task.key, title: task.title, column: task.column },
1474
+ task: { key: task.key, title: task.title, column: task.column, agentRole: agentRole || null },
1032
1475
  git: gitResult || { switched: false, reason: 'No repo found' },
1033
1476
  autoStashed: autoStash && localState === 'modified' && !alreadyOnBranch
1034
1477
  ? `Changes stashed before switch. Run: git stash list to see them.`
1035
1478
  : null,
1036
1479
  lastCommit: lastCommit || null,
1037
1480
  parkNote: task.parkNote || null,
1038
- cursorRules: cursorRulesFile ? { restored: true, path: cursorRulesFile } : { restored: false },
1481
+ cursorRules: cursorRulesFile
1482
+ ? { restored: true, path: cursorRulesFile, agentRole: agentRole || null }
1483
+ : { restored: false },
1039
1484
  message: gitResult?.switched || gitResult?.alreadyOnBranch
1040
- ? `You are on branch "${branch}". Ready to resume coding.`
1485
+ ? `You are on branch "${branch}". Ready to resume coding${agentRole ? ` as ${agentRole}` : ''}.`
1041
1486
  : `Branch switch failed — see git.manualSteps.`,
1042
- nextStep: task.column === 'in_review'
1487
+ nextStep: agentRole === 'builder'
1488
+ ? `BUILDER role active. Call claim_files to lock your files before editing.`
1489
+ : agentRole === 'scout'
1490
+ ? `SCOUT role active. Read-only mode — map the codebase and save findings with update_task(scoutReport=...).`
1491
+ : agentRole === 'coordinator'
1492
+ ? `COORDINATOR role active. Call decompose_task to plan parallel workstreams.`
1493
+ : agentRole === 'reviewer'
1494
+ ? `REVIEWER role active. Call review_pr to start the review chain.`
1495
+ : task.column === 'in_review'
1043
1496
  ? `Task is in review. Check if PR feedback needs addressing — call fix_pr_feedback if needed.`
1044
1497
  : task.parkNote?.remaining
1045
1498
  ? `Remaining: ${task.parkNote.remaining}`
@@ -1541,8 +1994,10 @@ Use this when a developer says "start task", "brief me on", or "what do I need t
1541
1994
  taskId: z.string().describe("Task's MongoDB ObjectId"),
1542
1995
  confirmed: z.boolean().optional().default(false).describe('Set true after reading the plan to move the task to in_progress'),
1543
1996
  repoPath: z.string().optional().describe('Absolute path to the local git repo (defaults to MCP process working directory). Used to write cursor rules file.'),
1997
+ agentRole: z.enum(['builder', 'reviewer', 'scout', 'coordinator']).optional()
1998
+ .describe('Set the agent role for this task session. Role-specific behavioral constraints are injected into cursor rules. builder=implements code, scout=reads/analyzes only, reviewer=reviews PRs only, coordinator=decomposes work.'),
1544
1999
  },
1545
- async ({ taskId, confirmed = false, repoPath }) => {
2000
+ async ({ taskId, confirmed = false, repoPath, agentRole }) => {
1546
2001
  const taskRes = await api.get(`/api/tasks/${taskId}`)
1547
2002
  if (!taskRes?.success) return errorText('Task not found')
1548
2003
  const task = taskRes.data.task
@@ -1722,6 +2177,21 @@ Use this when a developer says "start task", "brief me on", or "what do I need t
1722
2177
  ? `⏳ Plan is submitted and awaiting approval — you cannot create a branch until it is approved.`
1723
2178
  : `⚠️ Plan is not yet submitted for approval. Submit it first, then create the branch.`
1724
2179
  : null,
2180
+ decompositionWarning: (() => {
2181
+ if (!task.decomposition?.trim()) return null
2182
+ try {
2183
+ const dec = JSON.parse(task.decomposition)
2184
+ if (!dec.scoutReportPresent && !task.scoutReport?.trim())
2185
+ return `⚠️ This task has a decomposition plan but no scout report. Consider running scout_task before builders start.`
2186
+ return `Decomposition exists: ${dec.totalSubtasks} subtask(s), ${dec.executionOrder?.length || 0} execution group(s).`
2187
+ } catch { return null }
2188
+ })(),
2189
+ agentContext: {
2190
+ hasScoutReport: !!(task.scoutReport?.trim()),
2191
+ hasDecomposition: !!(task.decomposition?.trim()),
2192
+ claimedFiles: task.claimedFiles || [],
2193
+ agentRole: task.agentRole || null,
2194
+ },
1725
2195
  requiresConfirmation: true,
1726
2196
  message: approvalBlocks
1727
2197
  ? `Read the plan above, then follow workflowRoadmap — approval is required before you can branch and start coding.`
@@ -1773,10 +2243,18 @@ Use this when a developer says "start task", "brief me on", or "what do I need t
1773
2243
  } catch { /* might already be in_progress */ }
1774
2244
  }
1775
2245
 
1776
- // Write cursor rules file to local repo immediately on kickoff
2246
+ // Write cursor rules file to local repo immediately on kickoff (with role injection if set)
1777
2247
  let cursorRulesFile = null
1778
2248
  if (hasCursorRules) {
1779
- cursorRulesFile = writeCursorRulesFile(task.key, task.cursorRules, repoPath)
2249
+ cursorRulesFile = writeCursorRulesFile(task.key, task.cursorRules, repoPath, agentRole || null)
2250
+ } else if (agentRole) {
2251
+ // Even if no task-specific cursor rules, write role rules alone
2252
+ cursorRulesFile = writeCursorRulesFile(task.key, '(No task-specific rules — follow role constraints above.)', repoPath, agentRole)
2253
+ }
2254
+
2255
+ // Persist agentRole to server so board shows the active role
2256
+ if (agentRole) {
2257
+ api.patch(`/api/tasks/${taskId}`, { agentRole }).catch(() => {/* non-fatal */})
1780
2258
  }
1781
2259
 
1782
2260
  return text({
@@ -1787,14 +2265,23 @@ Use this when a developer says "start task", "brief me on", or "what do I need t
1787
2265
  column: moved ? 'in_progress' : task.column,
1788
2266
  branch: task.github?.headBranch || null,
1789
2267
  subtasks,
2268
+ agentRole: agentRole || null,
1790
2269
  },
1791
2270
  implementationPlan: hasReadme ? task.readmeMarkdown : null,
1792
- cursorRules: hasCursorRules
1793
- ? { active: true, rules: task.cursorRules, instruction: '⚠️ CURSOR RULES ACTIVE — You MUST follow every rule in the "rules" field for the entire duration of this task.' }
2271
+ cursorRules: hasCursorRules || agentRole
2272
+ ? {
2273
+ active: true,
2274
+ agentRole: agentRole || null,
2275
+ rules: task.cursorRules || null,
2276
+ roleRules: agentRole ? ROLE_RULES[agentRole] : null,
2277
+ instruction: agentRole
2278
+ ? `⚠️ AGENT ROLE: ${agentRole.toUpperCase()} — Follow the role behavioral constraints injected into the cursor rules file. These override default behavior.`
2279
+ : '⚠️ CURSOR RULES ACTIVE — You MUST follow every rule in the "rules" field for the entire duration of this task.',
2280
+ }
1794
2281
  : { active: false },
1795
2282
  cursorRulesFile: cursorRulesFile
1796
2283
  ? { written: true, path: cursorRulesFile, note: 'Rules file written to your repo. Cursor enforces it automatically on every prompt.' }
1797
- : hasCursorRules ? { written: false, note: 'Could not write rules file — not inside a git repo.' } : null,
2284
+ : (hasCursorRules || agentRole) ? { written: false, note: 'Could not write rules file — not inside a git repo.' } : null,
1798
2285
  recentCommits: recentCommits.slice(0, 5).map(c => ({
1799
2286
  sha: c.sha?.slice(0, 7),
1800
2287
  message: c.commit?.message?.split('\n')[0],
@@ -1803,7 +2290,15 @@ Use this when a developer says "start task", "brief me on", or "what do I need t
1803
2290
  })),
1804
2291
  movedToInProgress: moved,
1805
2292
  suggestedBranch: alreadyHasBranch ? null : suggestedBranch,
1806
- nextStep: alreadyHasBranch
2293
+ nextStep: agentRole === 'scout'
2294
+ ? `You are in SCOUT mode. Read the codebase, then save your findings with update_task(scoutReport=...). Do NOT modify any files.`
2295
+ : agentRole === 'coordinator'
2296
+ ? `You are in COORDINATOR mode. Read the README, then call decompose_task to break the work into parallel subtasks with file ownership.`
2297
+ : agentRole === 'builder'
2298
+ ? `You are in BUILDER mode. Call claim_files first, then start coding on "${alreadyHasBranch ? task.github.headBranch : suggestedBranch}".`
2299
+ : agentRole === 'reviewer'
2300
+ ? `You are in REVIEWER mode. Call review_pr to get the diff, then post_pr_review with your analysis.`
2301
+ : alreadyHasBranch
1807
2302
  ? `Branch "${task.github.headBranch}" already exists. Task is now In progress — start coding.`
1808
2303
  : `Call create_branch to create "${suggestedBranch}" — it will check your local git state and move the task to In progress automatically.`,
1809
2304
  })
@@ -2210,15 +2705,118 @@ function findRepoRoot(startPath) {
2210
2705
  return null
2211
2706
  }
2212
2707
 
2213
- /** Write task-specific cursor rules to .cursor/rules/<taskKey>.mdc in the local repo root. */
2214
- function writeCursorRulesFile(taskKey, rulesMarkdown, startPath) {
2708
+ // ── Agent role behavioral rule templates ─────────────────────────────────────
2709
+ // Prepended to cursor rules when an agentRole is set. Defines what the agent
2710
+ // CAN and CANNOT do for the duration of the task session.
2711
+ const ROLE_RULES = {
2712
+ builder: `## Agent Role: BUILDER
2713
+
2714
+ You are a BUILDER agent. Your behavioral constraints for this session:
2715
+
2716
+ **ALLOWED:**
2717
+ - Write, modify, and delete code files within the task scope
2718
+ - Create new files required by the implementation plan
2719
+ - Commit and push changes on the task branch
2720
+ - Run tests and fix failures
2721
+ - Claim file ownership before editing (use claim_files MCP tool)
2722
+
2723
+ **NOT ALLOWED:**
2724
+ - Modify files owned by another in-progress task (check claimedFiles conflicts)
2725
+ - Make architectural decisions not in the implementation plan — flag them instead
2726
+ - Edit files outside the task scope without explicit approval
2727
+ - Merge to main/master/dev directly
2728
+
2729
+ **WORK STYLE:**
2730
+ - Read the implementation plan fully before writing any code
2731
+ - Claim your files at the start with claim_files
2732
+ - Follow the spec precisely — don't add unrequested features
2733
+ - Commit atomically with conventional commit format (feat/fix/refactor)`,
2734
+
2735
+ scout: `## Agent Role: SCOUT
2736
+
2737
+ You are a SCOUT agent. Your behavioral constraints for this session:
2738
+
2739
+ **ALLOWED:**
2740
+ - Read and analyze any file in the codebase
2741
+ - Search for patterns, dependencies, and potential conflicts
2742
+ - Write a structured scout report (update_task with scoutReport field)
2743
+ - Identify which files the implementation will need to touch
2744
+ - Surface risks, gotchas, and architectural considerations
2745
+
2746
+ **NOT ALLOWED:**
2747
+ - Modify any source code files
2748
+ - Create branches or commits
2749
+ - Move tasks or create PRs
2750
+ - Make changes that would require a PR
2751
+
2752
+ **WORK STYLE:**
2753
+ - Your output is a scout report for the builder agents
2754
+ - Map file dependencies before builders start
2755
+ - Identify conflict risks with other in-progress tasks
2756
+ - Estimate complexity and flag potential blockers
2757
+ - Save your report with update_task(scoutReport=...)`,
2758
+
2759
+ reviewer: `## Agent Role: REVIEWER
2760
+
2761
+ You are a REVIEWER agent. Your behavioral constraints for this session:
2762
+
2763
+ **ALLOWED:**
2764
+ - Read all code changes and diffs (use review_pr)
2765
+ - Post structured reviews with specific analysis points (use post_pr_review)
2766
+ - Request changes or approve the PR
2767
+ - Reference specific files, functions, and line numbers in your review
2768
+
2769
+ **NOT ALLOWED:**
2770
+ - Approve a PR without reading every file in the diff
2771
+ - Write "looks good" or generic analysis — be specific and file-based
2772
+ - Merge the PR without completing the review chain (review_pr → post_pr_review → merge_pr)
2773
+ - Modify source code directly
2774
+
2775
+ **WORK STYLE:**
2776
+ - Always call review_pr first to get the full diff and spec
2777
+ - Check each file against the implementation plan
2778
+ - Build analysisPoints with file-specific findings (minimum 2)
2779
+ - Reference function names and line numbers
2780
+ - Only call merge_pr after post_pr_review returns a reviewId`,
2781
+
2782
+ coordinator: `## Agent Role: COORDINATOR
2783
+
2784
+ You are a COORDINATOR agent. Your behavioral constraints for this session:
2785
+
2786
+ **ALLOWED:**
2787
+ - Read the task implementation plan and codebase structure
2788
+ - Decompose the plan into parallel subtasks (use decompose_task)
2789
+ - Assign file ownership to each subtask with no overlaps
2790
+ - Assign roles to each subtask (builder, scout, reviewer)
2791
+ - Create subtasks on the board (use update_task with subtasks)
2792
+ - Monitor progress and resolve file conflicts
2793
+
2794
+ **NOT ALLOWED:**
2795
+ - Write implementation code directly
2796
+ - Claim file ownership for yourself
2797
+ - Bypass the decomposition step — always decompose before builders start
2798
+ - Start coding without a scout report when the codebase is unfamiliar
2799
+
2800
+ **WORK STYLE:**
2801
+ - Start with decompose_task to get the structured execution plan
2802
+ - Ensure no two builder subtasks claim the same file
2803
+ - Scout report must exist before builders begin
2804
+ - Each builder subtask must have explicit file ownership and a clear scope`,
2805
+ }
2806
+
2807
+ /** Write task-specific cursor rules to .cursor/rules/<taskKey>.mdc in the local repo root.
2808
+ * When role is provided, role-specific behavioral constraints are prepended. */
2809
+ function writeCursorRulesFile(taskKey, rulesMarkdown, startPath, role = null) {
2215
2810
  try {
2216
2811
  const repoRoot = findRepoRoot(startPath)
2217
2812
  if (!repoRoot) return null
2218
2813
  const rulesDir = join(repoRoot, '.cursor', 'rules')
2219
2814
  mkdirSync(rulesDir, { recursive: true })
2220
2815
  const filePath = join(rulesDir, `${taskKey.toLowerCase()}.mdc`)
2221
- const content = `---\ndescription: Task-specific rules for ${taskKey} — auto-generated by InternalTool MCP. Do not edit manually.\nalwaysApply: true\n---\n\n${rulesMarkdown}\n`
2816
+ const roleSection = role && ROLE_RULES[role]
2817
+ ? `${ROLE_RULES[role]}\n\n---\n\n## Task-Specific Rules\n\n`
2818
+ : ''
2819
+ const content = `---\ndescription: Task-specific rules for ${taskKey}${role ? ` (role: ${role})` : ''} — auto-generated by InternalTool MCP. Do not edit manually.\nalwaysApply: true\n---\n\n${roleSection}${rulesMarkdown}\n`
2222
2820
  writeFileSync(filePath, content, 'utf8')
2223
2821
  return filePath
2224
2822
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "internaltool-mcp",
3
- "version": "1.6.22",
3
+ "version": "1.6.24",
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",