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.
- package/index.js +650 -52
- 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
|
-
|
|
552
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
669
|
-
event:
|
|
670
|
-
body:
|
|
671
|
-
|
|
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
|
-
//
|
|
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:
|
|
1082
|
+
action: event,
|
|
687
1083
|
prNumber,
|
|
688
|
-
taskKey:
|
|
689
|
-
taskTitle:
|
|
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
|
|
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:
|
|
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:
|
|
1108
|
+
posted: true,
|
|
710
1109
|
event,
|
|
711
1110
|
prNumber,
|
|
712
|
-
|
|
713
|
-
|
|
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}"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
1023
|
-
|
|
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
|
|
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:
|
|
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
|
-
? {
|
|
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:
|
|
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
|
-
|
|
2214
|
-
|
|
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
|
|
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