internaltool-mcp 1.6.22 → 1.6.25
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 +910 -53
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -391,11 +391,16 @@ Set confirmed=false first to preview, then confirmed=true to execute everything.
|
|
|
391
391
|
// ── Save park note + server-side comment & notifications ──────────────
|
|
392
392
|
await api.patch(`/api/tasks/${taskId}/park`, { summary, remaining, blockers })
|
|
393
393
|
|
|
394
|
-
// ── Delete cursor rules file
|
|
394
|
+
// ── Delete cursor rules file + task-scoped workspace files ───────────────
|
|
395
395
|
let cursorRulesCleared = null
|
|
396
396
|
if (task?.cursorRules?.trim()) {
|
|
397
397
|
cursorRulesCleared = deleteCursorRulesFile(task.key, repoPath)
|
|
398
398
|
}
|
|
399
|
+
deleteCursorWorkspace(task?.agentRole || null, repoPath)
|
|
400
|
+
// Mark workspace as cleared in DB so UI shows "not active"
|
|
401
|
+
api.patch(`/api/tasks/${taskId}`, {
|
|
402
|
+
agentWorkspace: { clearedAt: new Date().toISOString() }
|
|
403
|
+
}).catch(() => {/* non-fatal */})
|
|
399
404
|
|
|
400
405
|
// ── #9 Capture last commit for handoff metadata ───────────────────────
|
|
401
406
|
const lastCommit = getLastCommitMeta(repoRoot)
|
|
@@ -434,8 +439,11 @@ Set confirmed=false first to read everything, then confirmed=true to execute.`,
|
|
|
434
439
|
confirmed: z.boolean().optional().default(false).describe('Set true to execute after reviewing the park note'),
|
|
435
440
|
repoPath: z.string().optional().describe('Absolute path to the local git repo (defaults to MCP process working directory).'),
|
|
436
441
|
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.'),
|
|
442
|
+
agentRole: z.enum(['builder', 'reviewer', 'scout', 'coordinator']).optional()
|
|
443
|
+
.describe('Set the agent role for this task session. Role-specific behavioral constraints are injected into cursor rules.'),
|
|
437
444
|
},
|
|
438
|
-
async ({ taskId, confirmed = false, repoPath, autoStash = false }) => {
|
|
445
|
+
async ({ taskId, confirmed = false, repoPath, autoStash = false, agentRole }) => {
|
|
446
|
+
trackTaskActivity(taskId, 'unpark_task')
|
|
439
447
|
const taskRes = await api.get(`/api/tasks/${taskId}`)
|
|
440
448
|
const task = taskRes?.data?.task
|
|
441
449
|
const branch = task?.github?.headBranch || null
|
|
@@ -546,10 +554,21 @@ Set confirmed=false first to read everything, then confirmed=true to execute.`,
|
|
|
546
554
|
// ── Clear park note + server-side comment & notification ──────────────
|
|
547
555
|
await api.patch(`/api/tasks/${taskId}/unpark`, {})
|
|
548
556
|
|
|
549
|
-
// ── Restore cursor rules file
|
|
557
|
+
// ── Restore cursor rules file (with role injection if set) ───────────────
|
|
550
558
|
let cursorRulesFile = null
|
|
551
|
-
|
|
552
|
-
|
|
559
|
+
const hasCursorRulesUnpark = task?.cursorRules?.trim()
|
|
560
|
+
if (hasCursorRulesUnpark || agentRole) {
|
|
561
|
+
cursorRulesFile = writeCursorRulesFile(
|
|
562
|
+
task.key,
|
|
563
|
+
hasCursorRulesUnpark || '(No task-specific rules — follow role constraints above.)',
|
|
564
|
+
repoPath,
|
|
565
|
+
agentRole || null
|
|
566
|
+
)
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Persist agentRole to server if set
|
|
570
|
+
if (agentRole) {
|
|
571
|
+
api.patch(`/api/tasks/${taskId}`, { agentRole }).catch(() => {/* non-fatal */})
|
|
553
572
|
}
|
|
554
573
|
|
|
555
574
|
// ── #9 Last commit metadata for handoff context ───────────────────────
|
|
@@ -557,17 +576,368 @@ Set confirmed=false first to read everything, then confirmed=true to execute.`,
|
|
|
557
576
|
|
|
558
577
|
return text({
|
|
559
578
|
unparked: true,
|
|
560
|
-
task: { key: task?.key, title: task?.title },
|
|
579
|
+
task: { key: task?.key, title: task?.title, agentRole: agentRole || null },
|
|
561
580
|
git: gitResult || { switched: false, reason: 'No branch linked or repo not found' },
|
|
562
581
|
autoStashed: autoStash ? 'Changes were auto-stashed before branch switch. Run: git stash list to see them.' : null,
|
|
563
582
|
lastCommit: lastCommit || null,
|
|
564
|
-
cursorRules: cursorRulesFile
|
|
583
|
+
cursorRules: cursorRulesFile
|
|
584
|
+
? { restored: true, path: cursorRulesFile, agentRole: agentRole || null }
|
|
585
|
+
: { restored: false },
|
|
565
586
|
commentPosted: true,
|
|
566
587
|
previousDevNotified: true,
|
|
567
588
|
parkNote: task?.parkNote || null,
|
|
568
589
|
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.`
|
|
590
|
+
? `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
591
|
: `Branch switch failed — see git.manualSteps. Cursor rules restored.`,
|
|
592
|
+
nextStep: agentRole === 'builder'
|
|
593
|
+
? `BUILDER role active. Call claim_files to lock your files before editing.`
|
|
594
|
+
: agentRole === 'scout'
|
|
595
|
+
? `SCOUT role active. Read-only mode — map the codebase and save findings with update_task(scoutReport=...).`
|
|
596
|
+
: agentRole === 'coordinator'
|
|
597
|
+
? `COORDINATOR role active. Call decompose_task to plan parallel workstreams.`
|
|
598
|
+
: agentRole === 'reviewer'
|
|
599
|
+
? `REVIEWER role active. Call review_pr to start the review chain.`
|
|
600
|
+
: null,
|
|
601
|
+
})
|
|
602
|
+
}
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
// ── claim_files ──────────────────────────────────────────────────────────────
|
|
606
|
+
server.tool(
|
|
607
|
+
'claim_files',
|
|
608
|
+
`Claim exclusive ownership of files for this task session.
|
|
609
|
+
|
|
610
|
+
Prevents merge conflicts by ensuring no two in-progress tasks edit the same file.
|
|
611
|
+
Call this at the start of a builder session before editing any files.
|
|
612
|
+
|
|
613
|
+
Returns a conflict list if any claimed files are already owned by another in-progress task.
|
|
614
|
+
Ownership is automatically released when the task is parked.
|
|
615
|
+
|
|
616
|
+
Roles: BUILDER agents MUST call this before editing. Coordinators call this as part of decompose_task.`,
|
|
617
|
+
{
|
|
618
|
+
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
619
|
+
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"])'),
|
|
620
|
+
},
|
|
621
|
+
async ({ taskId, files }) => {
|
|
622
|
+
trackTaskActivity(taskId, 'claim_files')
|
|
623
|
+
const res = await api.post(`/api/tasks/${taskId}/files/claim`, { files })
|
|
624
|
+
if (!res?.success) {
|
|
625
|
+
if (res?.conflicts) {
|
|
626
|
+
return text({
|
|
627
|
+
blocked: true,
|
|
628
|
+
reason: 'File ownership conflict — these files are already claimed by another in-progress task.',
|
|
629
|
+
conflicts: res.conflicts,
|
|
630
|
+
message: 'Wait for the conflicting task to release the files, or coordinate with the other developer to resolve the overlap.',
|
|
631
|
+
})
|
|
632
|
+
}
|
|
633
|
+
return errorText(res?.message || 'Could not claim files')
|
|
634
|
+
}
|
|
635
|
+
return text({
|
|
636
|
+
claimed: true,
|
|
637
|
+
files,
|
|
638
|
+
taskId,
|
|
639
|
+
message: `${files.length} file(s) claimed. No other in-progress task in this project may edit these files until you release or park.`,
|
|
640
|
+
nextStep: 'You may now safely edit the claimed files. Files are auto-released when you call park_task.',
|
|
641
|
+
})
|
|
642
|
+
}
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
// ── release_files ─────────────────────────────────────────────────────────────
|
|
646
|
+
server.tool(
|
|
647
|
+
'release_files',
|
|
648
|
+
`Release all file ownership claims for a task.
|
|
649
|
+
|
|
650
|
+
Call this when a builder subtask is complete and another task needs to edit the same files.
|
|
651
|
+
Files are also released automatically when the task is parked.`,
|
|
652
|
+
{
|
|
653
|
+
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
654
|
+
},
|
|
655
|
+
async ({ taskId }) => {
|
|
656
|
+
const res = await api.post(`/api/tasks/${taskId}/files/release`, {})
|
|
657
|
+
if (!res?.success) return errorText(res?.message || 'Could not release files')
|
|
658
|
+
return text({
|
|
659
|
+
released: true,
|
|
660
|
+
taskId,
|
|
661
|
+
message: 'All file claims released. Other tasks may now claim these files.',
|
|
662
|
+
})
|
|
663
|
+
}
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
// ── decompose_task ────────────────────────────────────────────────────────────
|
|
667
|
+
server.tool(
|
|
668
|
+
'decompose_task',
|
|
669
|
+
`Coordinator tool: decompose an implementation plan into parallel subtasks with file ownership and role assignments.
|
|
670
|
+
|
|
671
|
+
Use this when you are acting as a COORDINATOR agent and need to break a large task into
|
|
672
|
+
parallel workstreams that multiple builder agents can execute without stepping on each other.
|
|
673
|
+
|
|
674
|
+
What this does:
|
|
675
|
+
1. Reads the task's implementation plan (README)
|
|
676
|
+
2. Analyzes the plan to identify independent work units
|
|
677
|
+
3. Returns a structured execution plan with:
|
|
678
|
+
- Parallel subtask groups (tasks that can run simultaneously)
|
|
679
|
+
- File ownership per subtask (no overlaps — each file appears in exactly one subtask)
|
|
680
|
+
- Role assignment per subtask (builder/scout/reviewer)
|
|
681
|
+
- Dependency order (which subtasks must complete before others start)
|
|
682
|
+
4. Saves the decomposition plan to the task (decomposition field)
|
|
683
|
+
5. Optionally creates the subtasks on the board
|
|
684
|
+
|
|
685
|
+
REQUIRED BEFORE BUILDERS START:
|
|
686
|
+
- A scout report should exist (call kickoff_task with agentRole=scout first)
|
|
687
|
+
- The README must describe what needs to be built
|
|
688
|
+
|
|
689
|
+
Call confirmed=false to preview the decomposition, confirmed=true to save it.`,
|
|
690
|
+
{
|
|
691
|
+
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
692
|
+
subtaskPlan: z.array(z.object({
|
|
693
|
+
title: z.string().describe('Subtask title'),
|
|
694
|
+
role: z.enum(['builder', 'scout', 'reviewer']).describe('Agent role for this subtask'),
|
|
695
|
+
files: z.array(z.string()).describe('Files this subtask exclusively owns (no overlap with other subtasks)'),
|
|
696
|
+
description: z.string().describe('What this subtask implements'),
|
|
697
|
+
dependsOn: z.array(z.string()).optional().describe('Titles of subtasks that must complete before this one starts'),
|
|
698
|
+
parallel: z.boolean().optional().default(true).describe('Can this subtask run in parallel with others at the same level?'),
|
|
699
|
+
})).min(1).describe('Decomposed subtask execution plan. File ownership must not overlap between subtasks.'),
|
|
700
|
+
confirmed: z.boolean().optional().default(false).describe('Set true to save the decomposition and create subtasks on the board'),
|
|
701
|
+
},
|
|
702
|
+
async ({ taskId, subtaskPlan, confirmed = false }) => {
|
|
703
|
+
trackTaskActivity(taskId, 'decompose_task')
|
|
704
|
+
const taskRes = await apiWithRetry(() => api.get(`/api/tasks/${taskId}`))
|
|
705
|
+
if (!taskRes?.success) return errorText('Task not found')
|
|
706
|
+
const task = taskRes.data.task
|
|
707
|
+
|
|
708
|
+
// Validate: no file overlap between subtasks
|
|
709
|
+
const fileOwnershipMap = {}
|
|
710
|
+
const conflicts = []
|
|
711
|
+
for (const subtask of subtaskPlan) {
|
|
712
|
+
for (const file of (subtask.files || [])) {
|
|
713
|
+
if (fileOwnershipMap[file]) {
|
|
714
|
+
conflicts.push({ file, claimedBy: [fileOwnershipMap[file], subtask.title] })
|
|
715
|
+
} else {
|
|
716
|
+
fileOwnershipMap[file] = subtask.title
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
if (conflicts.length > 0) {
|
|
722
|
+
return text({
|
|
723
|
+
blocked: true,
|
|
724
|
+
reason: 'File ownership overlap detected — each file must appear in exactly one subtask.',
|
|
725
|
+
conflicts,
|
|
726
|
+
message: 'Fix the overlapping file assignments before confirming the decomposition.',
|
|
727
|
+
})
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Group subtasks by dependency level for parallel execution visualization
|
|
731
|
+
const parallelGroups = []
|
|
732
|
+
const placed = new Set()
|
|
733
|
+
let safetyLimit = subtaskPlan.length + 1
|
|
734
|
+
while (placed.size < subtaskPlan.length && safetyLimit-- > 0) {
|
|
735
|
+
const batch = subtaskPlan.filter(s =>
|
|
736
|
+
!placed.has(s.title) &&
|
|
737
|
+
(s.dependsOn || []).every(dep => placed.has(dep))
|
|
738
|
+
)
|
|
739
|
+
if (!batch.length) break
|
|
740
|
+
parallelGroups.push(batch.map(s => s.title))
|
|
741
|
+
batch.forEach(s => placed.add(s.title))
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const executionPlan = {
|
|
745
|
+
taskKey: task.key,
|
|
746
|
+
taskTitle: task.title,
|
|
747
|
+
totalSubtasks: subtaskPlan.length,
|
|
748
|
+
subtasks: subtaskPlan.map(s => ({
|
|
749
|
+
title: s.title,
|
|
750
|
+
role: s.role,
|
|
751
|
+
files: s.files,
|
|
752
|
+
description: s.description,
|
|
753
|
+
dependsOn: s.dependsOn || [],
|
|
754
|
+
parallel: s.parallel !== false,
|
|
755
|
+
})),
|
|
756
|
+
executionOrder: parallelGroups,
|
|
757
|
+
fileOwnershipMap,
|
|
758
|
+
scoutReportPresent: !!(task.scoutReport?.trim()),
|
|
759
|
+
readmePresent: !!(task.readmeMarkdown?.trim()),
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
if (!confirmed) {
|
|
763
|
+
return text({
|
|
764
|
+
decomposition: executionPlan,
|
|
765
|
+
warnings: [
|
|
766
|
+
!executionPlan.scoutReportPresent ? '⚠️ No scout report on this task. Consider running a scout agent first to map the codebase before builders start.' : null,
|
|
767
|
+
!executionPlan.readmePresent ? '⚠️ No implementation plan (README). Builders need a spec to work from.' : null,
|
|
768
|
+
].filter(Boolean),
|
|
769
|
+
requiresConfirmation: true,
|
|
770
|
+
message: 'Review the decomposition above. Call decompose_task again with confirmed=true to save it and create the subtasks on the board.',
|
|
771
|
+
})
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// Save decomposition to task
|
|
775
|
+
const decompositionJson = JSON.stringify(executionPlan, null, 2)
|
|
776
|
+
try {
|
|
777
|
+
await api.patch(`/api/tasks/${taskId}`, { decomposition: decompositionJson })
|
|
778
|
+
} catch { /* non-fatal — decomposition is returned regardless */ }
|
|
779
|
+
|
|
780
|
+
// Create subtasks on the board
|
|
781
|
+
const currentSubtasks = task.subtasks || []
|
|
782
|
+
const newSubtasks = [
|
|
783
|
+
...currentSubtasks,
|
|
784
|
+
...subtaskPlan.map((s, i) => ({
|
|
785
|
+
title: `[${s.role.toUpperCase()}] ${s.title}`,
|
|
786
|
+
done: false,
|
|
787
|
+
order: currentSubtasks.length + i,
|
|
788
|
+
})),
|
|
789
|
+
]
|
|
790
|
+
try {
|
|
791
|
+
await api.patch(`/api/tasks/${taskId}`, { subtasks: newSubtasks })
|
|
792
|
+
} catch { /* non-fatal */ }
|
|
793
|
+
|
|
794
|
+
return text({
|
|
795
|
+
decomposed: true,
|
|
796
|
+
taskKey: task.key,
|
|
797
|
+
executionPlan,
|
|
798
|
+
subtasksCreated: subtaskPlan.length,
|
|
799
|
+
message: `Decomposition saved. ${subtaskPlan.length} subtask(s) added to the board.`,
|
|
800
|
+
nextStep: parallelGroups.length > 0
|
|
801
|
+
? `Start with parallel group 1: ${parallelGroups[0].join(', ')}. Each builder agent should call kickoff_task with agentRole="builder" and claim_files before coding.`
|
|
802
|
+
: 'Start the subtasks in order. Each builder should call kickoff_task with agentRole="builder" and claim_files.',
|
|
803
|
+
})
|
|
804
|
+
}
|
|
805
|
+
)
|
|
806
|
+
|
|
807
|
+
// ── scout_task ────────────────────────────────────────────────────────────────
|
|
808
|
+
server.tool(
|
|
809
|
+
'scout_task',
|
|
810
|
+
`Scout agent entry point: analyze the codebase for a task and save a structured scout report.
|
|
811
|
+
|
|
812
|
+
Call this BEFORE builders start when the Coordinator needs a codebase map.
|
|
813
|
+
Use agentRole="scout" in kickoff_task first, then call scout_task to get your briefing.
|
|
814
|
+
|
|
815
|
+
Two-phase flow:
|
|
816
|
+
Phase 1 — confirmed=false: get the briefing (what to analyze, report format)
|
|
817
|
+
Phase 2 — confirmed=true + report: save your findings to the task
|
|
818
|
+
|
|
819
|
+
The scout report is consumed by:
|
|
820
|
+
- decompose_task (warns if missing)
|
|
821
|
+
- get_agent_context (included in builder context)
|
|
822
|
+
- kickoff_task (surfaced as part of builder brief)
|
|
823
|
+
|
|
824
|
+
Scouts MUST NOT modify any source code files or create branches.`,
|
|
825
|
+
{
|
|
826
|
+
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
827
|
+
confirmed: z.boolean().optional().default(false).describe('Set true to save the report. Set false (default) to get the briefing on what to analyze.'),
|
|
828
|
+
report: z.string().optional().default('').describe('Your structured scout findings in markdown (required when confirmed=true)'),
|
|
829
|
+
},
|
|
830
|
+
async ({ taskId, confirmed = false, report = '' }) => {
|
|
831
|
+
trackTaskActivity(taskId, 'scout_task')
|
|
832
|
+
const taskRes = await apiWithRetry(() => api.get(`/api/tasks/${taskId}`))
|
|
833
|
+
if (!taskRes?.success) return errorText('Task not found')
|
|
834
|
+
const task = taskRes.data.task
|
|
835
|
+
|
|
836
|
+
if (!confirmed) {
|
|
837
|
+
return text({
|
|
838
|
+
brief: {
|
|
839
|
+
key: task.key,
|
|
840
|
+
title: task.title,
|
|
841
|
+
implementationPlan: task.readmeMarkdown || '(no README — ask coordinator to write one first)',
|
|
842
|
+
},
|
|
843
|
+
currentScoutReport: task.scoutReport?.trim() || null,
|
|
844
|
+
scoutInstructions: {
|
|
845
|
+
objective: 'Map the codebase as it relates to this task. Identify every file that will need to change.',
|
|
846
|
+
analyzeThese: [
|
|
847
|
+
'Entry points — where does execution start for this feature area?',
|
|
848
|
+
'File dependency graph — which files import from which?',
|
|
849
|
+
'Existing patterns — what conventions does this codebase use (naming, folder structure, abstractions)?',
|
|
850
|
+
'Conflict risks — are any in-progress tasks touching the same files?',
|
|
851
|
+
'Complexity estimate — lines of code, number of files, coupling, test coverage',
|
|
852
|
+
],
|
|
853
|
+
reportFormat: {
|
|
854
|
+
summary: 'One paragraph overview of what this task touches in the codebase',
|
|
855
|
+
entryPoints: ['file/path/entry.ts', '...'],
|
|
856
|
+
keyFiles: [{ path: 'file/path.ts', role: 'what it does for this task' }],
|
|
857
|
+
risks: ['Potential gotchas, breaking changes, or hidden complexity'],
|
|
858
|
+
suggestedFileOwnership: { 'Subtask Name': ['file1.ts', 'file2.ts'] },
|
|
859
|
+
complexityEstimate: 'low | medium | high',
|
|
860
|
+
},
|
|
861
|
+
},
|
|
862
|
+
requiresConfirmation: true,
|
|
863
|
+
message: `Analyze the codebase for ${task.key}, then call scout_task again with confirmed=true and your report string.`,
|
|
864
|
+
})
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
if (!report.trim()) return errorText('report is required when confirmed=true. Pass your scout findings as a markdown string.')
|
|
868
|
+
|
|
869
|
+
try {
|
|
870
|
+
await api.patch(`/api/tasks/${taskId}`, { scoutReport: report })
|
|
871
|
+
} catch (e) {
|
|
872
|
+
return errorText(`Failed to save scout report: ${e?.message || 'unknown error'}`)
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
return text({
|
|
876
|
+
saved: true,
|
|
877
|
+
taskKey: task.key,
|
|
878
|
+
message: `Scout report saved for ${task.key}.`,
|
|
879
|
+
nextStep: 'The coordinator can now call decompose_task — the scout report will be included in builder context automatically via get_agent_context.',
|
|
880
|
+
})
|
|
881
|
+
}
|
|
882
|
+
)
|
|
883
|
+
|
|
884
|
+
// ── get_agent_context ─────────────────────────────────────────────────────────
|
|
885
|
+
server.tool(
|
|
886
|
+
'get_agent_context',
|
|
887
|
+
`Get a complete context package for starting work on a task as a specific agent role.
|
|
888
|
+
|
|
889
|
+
Call this instead of get_task when starting a session — it returns everything composed and
|
|
890
|
+
ready to use: systemPrompt, role behavioral constraints, implementation plan, cursor rules,
|
|
891
|
+
scout report, decomposition plan, claimed files, and project-level custom guidance.
|
|
892
|
+
|
|
893
|
+
Use this as the single entry point for any agent picking up a task. Replaces the need to
|
|
894
|
+
call get_task + kickoff_task preview separately.
|
|
895
|
+
|
|
896
|
+
Returns systemPrompt ready to use as a Claude system prompt.`,
|
|
897
|
+
{
|
|
898
|
+
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
899
|
+
role: z.enum(['builder', 'reviewer', 'scout', 'coordinator']).optional()
|
|
900
|
+
.describe('Agent role for this session. Falls back to task.agentRole if omitted.'),
|
|
901
|
+
},
|
|
902
|
+
async ({ taskId, role }) => {
|
|
903
|
+
trackTaskActivity(taskId, 'get_agent_context')
|
|
904
|
+
const qs = role ? `?role=${role}` : ''
|
|
905
|
+
const res = await apiWithRetry(() => api.get(`/api/tasks/${taskId}/agent-context${qs}`))
|
|
906
|
+
if (!res?.success) return errorText(res?.message || 'Failed to get agent context')
|
|
907
|
+
const ctx = res.data
|
|
908
|
+
|
|
909
|
+
const effectiveRole = ctx.role
|
|
910
|
+
const roleRules = effectiveRole && ROLE_RULES[effectiveRole] ? ROLE_RULES[effectiveRole] : null
|
|
911
|
+
|
|
912
|
+
// Compose the full system prompt
|
|
913
|
+
const parts = []
|
|
914
|
+
if (effectiveRole) {
|
|
915
|
+
const roleLabel = ctx.customRoleLabel || effectiveRole.toUpperCase()
|
|
916
|
+
parts.push(`You are a ${roleLabel} agent working on task ${ctx.taskKey}: "${ctx.taskTitle}".`)
|
|
917
|
+
}
|
|
918
|
+
if (roleRules) parts.push(roleRules)
|
|
919
|
+
if (ctx.customPromptHint) parts.push(`\n## Project-Specific Guidance\n\n${ctx.customPromptHint}`)
|
|
920
|
+
if (ctx.task?.cursorRules?.trim()) parts.push(`\n## Task-Specific Cursor Rules\n\n${ctx.task.cursorRules}`)
|
|
921
|
+
if (ctx.task?.readmeMarkdown?.trim()) parts.push(`\n## Implementation Plan\n\n${ctx.task.readmeMarkdown}`)
|
|
922
|
+
if (ctx.task?.scoutReport?.trim()) parts.push(`\n## Scout Report (Codebase Analysis)\n\n${ctx.task.scoutReport}`)
|
|
923
|
+
if (ctx.task?.decomposition?.trim()) {
|
|
924
|
+
try {
|
|
925
|
+
const dec = JSON.parse(ctx.task.decomposition)
|
|
926
|
+
parts.push(`\n## Execution Plan\n\nExecution order: ${JSON.stringify(dec.executionOrder)}\nFile ownership: ${JSON.stringify(dec.fileOwnershipMap, null, 2)}`)
|
|
927
|
+
} catch { /* non-fatal */ }
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
return text({
|
|
931
|
+
role: effectiveRole,
|
|
932
|
+
taskKey: ctx.taskKey,
|
|
933
|
+
taskTitle: ctx.taskTitle,
|
|
934
|
+
systemPrompt: parts.join('\n\n'),
|
|
935
|
+
allowedTools: ctx.customAllowedTools || null,
|
|
936
|
+
claimedFiles: ctx.task?.claimedFiles || [],
|
|
937
|
+
warnings: ctx.warnings || [],
|
|
938
|
+
task: ctx.task,
|
|
939
|
+
project: ctx.project,
|
|
940
|
+
usage: 'Use systemPrompt as your Claude system prompt. If allowedTools is set, restrict your MCP tool calls to that list.',
|
|
571
941
|
})
|
|
572
942
|
}
|
|
573
943
|
)
|
|
@@ -579,7 +949,17 @@ Set confirmed=false first to read everything, then confirmed=true to execute.`,
|
|
|
579
949
|
|
|
580
950
|
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
951
|
|
|
582
|
-
|
|
952
|
+
MANDATORY: After calling this tool, you MUST:
|
|
953
|
+
1. Read every file in diff.files — check the patch hunk line by line
|
|
954
|
+
2. Compare each change against spec.implementationPlan — does the code implement what was planned?
|
|
955
|
+
3. Identify what is present, what is missing, what is wrong
|
|
956
|
+
4. Build analysisPoints (min 2) referencing specific filenames and function names
|
|
957
|
+
5. Only THEN call post_pr_review with your verdict and analysisPoints
|
|
958
|
+
|
|
959
|
+
Do NOT call post_pr_review immediately after this without analyzing the diff first.
|
|
960
|
+
Do NOT approve a PR without reading the patch hunks.
|
|
961
|
+
|
|
962
|
+
Use this when the developer asks "review PR", "check the PR for TASK-X", or "is this PR ready to merge".`,
|
|
583
963
|
{
|
|
584
964
|
taskId: z.string().describe("Task's MongoDB ObjectId (used to find the project + PR number)"),
|
|
585
965
|
},
|
|
@@ -654,48 +1034,75 @@ Call this FIRST before post_pr_review. Use it when the developer asks "review PR
|
|
|
654
1034
|
// ── post_pr_review ────────────────────────────────────────────────────────────
|
|
655
1035
|
server.tool(
|
|
656
1036
|
'post_pr_review',
|
|
657
|
-
`Post a GitHub PR review after analyzing the code diff.
|
|
1037
|
+
`Post a GitHub PR review after analyzing the code diff from review_pr.
|
|
658
1038
|
|
|
659
1039
|
Supports three actions:
|
|
660
1040
|
- APPROVE — Approves the PR. Notifies the developer they can merge.
|
|
661
1041
|
- REQUEST_CHANGES — Blocks merge. Developer gets notified. Task board updated.
|
|
662
1042
|
- COMMENT — Leaves a comment without approving or blocking.
|
|
663
1043
|
|
|
664
|
-
|
|
1044
|
+
REQUIRED WORKFLOW — you MUST call review_pr first and read the diff before calling this.
|
|
1045
|
+
You MUST populate analysisPoints with specific findings from the diff — what you checked,
|
|
1046
|
+
what matched the spec, what is missing or broken. Generic analysis like "code looks good"
|
|
1047
|
+
will be rejected. Be file-specific: reference filenames, function names, line numbers.
|
|
665
1048
|
|
|
666
|
-
|
|
1049
|
+
Human approval gate: confirmed=false shows preview, confirmed=true posts to GitHub.
|
|
1050
|
+
|
|
1051
|
+
Returns reviewId — save it and pass it to merge_pr to prove semantic review happened.`,
|
|
667
1052
|
{
|
|
668
|
-
taskId:
|
|
669
|
-
event:
|
|
670
|
-
body:
|
|
671
|
-
|
|
1053
|
+
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
1054
|
+
event: z.enum(['APPROVE', 'REQUEST_CHANGES', 'COMMENT']).describe('Review action'),
|
|
1055
|
+
body: z.string().describe('Review summary posted to GitHub — be specific about files, functions, issues'),
|
|
1056
|
+
analysisPoints: z.array(z.string()).min(2).describe(
|
|
1057
|
+
'Structured findings from your diff analysis. Each entry must reference a specific file, function, or line. ' +
|
|
1058
|
+
'Examples: ["calculator.js: add/subtract/multiply all present and correct", ' +
|
|
1059
|
+
'"divide() function missing — spec requires divide-by-zero handling", ' +
|
|
1060
|
+
'"No input validation on any function — null/string inputs silently coerce"]. ' +
|
|
1061
|
+
'Minimum 2 points required. Generic entries like "code looks fine" are rejected.'
|
|
1062
|
+
),
|
|
1063
|
+
confirmed: z.boolean().optional().default(false).describe('Set true after the human has read and approved the review text'),
|
|
672
1064
|
},
|
|
673
|
-
async ({ taskId, event, body: reviewBody, confirmed = false }) => {
|
|
1065
|
+
async ({ taskId, event, body: reviewBody, analysisPoints, confirmed = false }) => {
|
|
674
1066
|
const taskRes = await apiWithRetry(() => api.get(`/api/tasks/${taskId}`))
|
|
675
1067
|
if (!taskRes?.success) return errorText('Task not found')
|
|
676
1068
|
const task = taskRes.data.task
|
|
677
1069
|
|
|
678
1070
|
const prNumber = task.github?.prNumber
|
|
679
1071
|
const projectId = task.project?._id || task.project
|
|
680
|
-
if (!prNumber) return errorText('No PR linked to this task.')
|
|
1072
|
+
if (!prNumber) return errorText('No PR linked to this task. Call raise_pr first.')
|
|
681
1073
|
|
|
682
|
-
//
|
|
1074
|
+
// Enforce that analysisPoints are specific — reject obviously generic ones
|
|
1075
|
+
const genericPhrases = ['looks good', 'lgtm', 'code is fine', 'no issues', 'all good', 'seems correct']
|
|
1076
|
+
const tooGeneric = analysisPoints.filter(p =>
|
|
1077
|
+
p.trim().length < 20 || genericPhrases.some(g => p.toLowerCase().includes(g))
|
|
1078
|
+
)
|
|
1079
|
+
if (tooGeneric.length > 0) {
|
|
1080
|
+
return text({
|
|
1081
|
+
blocked: true,
|
|
1082
|
+
reason: 'analysisPoints must be file-specific and reference actual diff content.',
|
|
1083
|
+
rejected: tooGeneric,
|
|
1084
|
+
instruction: 'Call review_pr first to get the diff, then re-examine each file and describe specific findings.',
|
|
1085
|
+
})
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// Preview — human must confirm
|
|
683
1089
|
if (!confirmed) {
|
|
684
1090
|
return text({
|
|
685
1091
|
preview: {
|
|
686
|
-
action:
|
|
1092
|
+
action: event,
|
|
687
1093
|
prNumber,
|
|
688
|
-
taskKey:
|
|
689
|
-
taskTitle:
|
|
1094
|
+
taskKey: task.key,
|
|
1095
|
+
taskTitle: task.title,
|
|
690
1096
|
reviewBody,
|
|
1097
|
+
analysisPoints,
|
|
691
1098
|
},
|
|
692
1099
|
warning: event === 'APPROVE'
|
|
693
1100
|
? '✅ This will APPROVE the PR on GitHub and notify the developer they can merge.'
|
|
694
1101
|
: event === 'REQUEST_CHANGES'
|
|
695
|
-
? '⚠️ This will REQUEST CHANGES — the developer will be blocked from merging until they fix the issues
|
|
1102
|
+
? '⚠️ This will REQUEST CHANGES — the developer will be blocked from merging until they fix the issues.'
|
|
696
1103
|
: 'ℹ️ This will post a comment on the PR without approving or blocking.',
|
|
697
1104
|
requiresConfirmation: true,
|
|
698
|
-
message:
|
|
1105
|
+
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
1106
|
})
|
|
700
1107
|
}
|
|
701
1108
|
|
|
@@ -705,20 +1112,24 @@ IMPORTANT: This posts a real GitHub review and updates the InternalTool board. T
|
|
|
705
1112
|
)
|
|
706
1113
|
if (!res?.success) return errorText(res?.message || 'Could not post review')
|
|
707
1114
|
|
|
1115
|
+
const reviewId = res.data?.reviewId
|
|
1116
|
+
|
|
708
1117
|
return text({
|
|
709
|
-
posted:
|
|
1118
|
+
posted: true,
|
|
710
1119
|
event,
|
|
711
1120
|
prNumber,
|
|
712
|
-
|
|
713
|
-
|
|
1121
|
+
reviewId,
|
|
1122
|
+
taskKey: task.key,
|
|
1123
|
+
analysisPoints,
|
|
1124
|
+
message: event === 'APPROVE'
|
|
714
1125
|
? `✅ PR #${prNumber} approved. Developer has been notified and can now merge.`
|
|
715
1126
|
: event === 'REQUEST_CHANGES'
|
|
716
1127
|
? `⚠️ Changes requested on PR #${prNumber}. Developer has been notified. Task board updated.`
|
|
717
1128
|
: `💬 Comment posted on PR #${prNumber}.`,
|
|
718
1129
|
nextStep: event === 'APPROVE'
|
|
719
|
-
? `Call merge_pr with taskId="${taskId}"
|
|
1130
|
+
? `Call merge_pr with taskId="${taskId}" and reviewId="${reviewId}" to merge.`
|
|
720
1131
|
: event === 'REQUEST_CHANGES'
|
|
721
|
-
? `Wait for the developer to push fixes. They'll call fix_pr_feedback, then re-push. You'll
|
|
1132
|
+
? `Wait for the developer to push fixes. They'll call fix_pr_feedback, then re-push. You'll be notified.`
|
|
722
1133
|
: null,
|
|
723
1134
|
})
|
|
724
1135
|
}
|
|
@@ -727,28 +1138,35 @@ IMPORTANT: This posts a real GitHub review and updates the InternalTool board. T
|
|
|
727
1138
|
// ── merge_pr ─────────────────────────────────────────────────────────────────
|
|
728
1139
|
server.tool(
|
|
729
1140
|
'merge_pr',
|
|
730
|
-
`Merge a pull request after
|
|
1141
|
+
`Merge a pull request after semantic review and human approval.
|
|
1142
|
+
|
|
1143
|
+
REQUIRED WORKFLOW:
|
|
1144
|
+
1. Call review_pr — read the diff and spec
|
|
1145
|
+
2. Call post_pr_review — post your verdict with specific analysisPoints, get back reviewId
|
|
1146
|
+
3. Call merge_pr — pass reviewId to prove semantic review happened
|
|
1147
|
+
|
|
1148
|
+
If the PR already has GitHub approvals from before this session, reviewId is not required.
|
|
731
1149
|
|
|
732
1150
|
Safety checks (pre-merge):
|
|
1151
|
+
- Semantic review: reviewId required if PR has no existing GitHub approvals
|
|
733
1152
|
- PR must be open and not already merged
|
|
734
1153
|
- No pending change requests
|
|
735
1154
|
- CI checks must be passing (pass skipChecks=true to override)
|
|
736
1155
|
|
|
737
1156
|
Human approval gate:
|
|
738
|
-
- confirmed=false (default): shows
|
|
1157
|
+
- confirmed=false (default): shows all checks and asks for confirmation
|
|
739
1158
|
- confirmed=true: executes the merge
|
|
740
1159
|
|
|
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".`,
|
|
1160
|
+
The task moves to Done automatically via the GitHub webhook.`,
|
|
744
1161
|
{
|
|
745
1162
|
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
746
1163
|
confirmed: z.boolean().optional().default(false).describe('Set true only after the human has reviewed the safety checks and approved the merge'),
|
|
747
1164
|
mergeMethod: z.enum(['squash', 'merge', 'rebase']).optional().default('squash').describe('Merge strategy (default: squash)'),
|
|
748
1165
|
commitTitle: z.string().optional().describe('Override the merge commit title. Defaults to PR title (#number).'),
|
|
749
1166
|
skipChecks: z.boolean().optional().default(false).describe('Skip CI check gate — use only when checks are informational or flaky'),
|
|
1167
|
+
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
1168
|
},
|
|
751
|
-
async ({ taskId, confirmed = false, mergeMethod = 'squash', commitTitle, skipChecks = false }) => {
|
|
1169
|
+
async ({ taskId, confirmed = false, mergeMethod = 'squash', commitTitle, skipChecks = false, reviewId }) => {
|
|
752
1170
|
const taskRes = await apiWithRetry(() => api.get(`/api/tasks/${taskId}`))
|
|
753
1171
|
if (!taskRes?.success) return errorText('Task not found')
|
|
754
1172
|
const task = taskRes.data.task
|
|
@@ -769,8 +1187,30 @@ Use this when the reviewer says "merge it", "looks good, ship it", or "merge the
|
|
|
769
1187
|
return text({ blocked: true, reason: `PR #${prNumber} is ${pr.state} — cannot merge a closed PR.` })
|
|
770
1188
|
}
|
|
771
1189
|
|
|
1190
|
+
// ── Semantic review gate ──────────────────────────────────────────────────
|
|
1191
|
+
// If the PR has no GitHub approvals from any session, a reviewId from
|
|
1192
|
+
// post_pr_review in this session is mandatory. This ensures code was
|
|
1193
|
+
// actually read and analyzed before merge, not just rubber-stamped.
|
|
1194
|
+
const hasExistingApproval = pr.approvals > 0
|
|
1195
|
+
const hasSessionReview = !!reviewId
|
|
1196
|
+
if (!hasExistingApproval && !hasSessionReview) {
|
|
1197
|
+
return text({
|
|
1198
|
+
blocked: true,
|
|
1199
|
+
reason: 'No semantic review has been performed on this PR.',
|
|
1200
|
+
required: [
|
|
1201
|
+
`1. Call review_pr with taskId="${taskId}" to read the diff and spec`,
|
|
1202
|
+
`2. Analyze the diff — check every file against the implementation plan`,
|
|
1203
|
+
`3. Call post_pr_review with taskId="${taskId}", your verdict, and specific analysisPoints`,
|
|
1204
|
+
`4. Pass the returned reviewId here to prove the review happened`,
|
|
1205
|
+
],
|
|
1206
|
+
note: 'If this PR was already approved by a reviewer on GitHub, merge_pr will allow it through automatically.',
|
|
1207
|
+
})
|
|
1208
|
+
}
|
|
1209
|
+
|
|
772
1210
|
// Build safety check summary
|
|
773
1211
|
const safetyChecks = [
|
|
1212
|
+
{ check: 'Semantic review performed', pass: hasExistingApproval || hasSessionReview,
|
|
1213
|
+
note: hasSessionReview ? `reviewId ${reviewId} from this session` : hasExistingApproval ? `${pr.approvals} existing GitHub approval(s)` : 'Missing — call review_pr then post_pr_review' },
|
|
774
1214
|
{ check: 'PR is open', pass: pr.state === 'open' },
|
|
775
1215
|
{ check: 'Not already merged', pass: !pr.merged },
|
|
776
1216
|
{ check: 'No change requests', pass: pr.changesRequested === 0,
|
|
@@ -858,8 +1298,10 @@ Flow:
|
|
|
858
1298
|
confirmed: z.boolean().optional().default(false).describe('Set true after reviewing the context to execute the resume'),
|
|
859
1299
|
repoPath: z.string().optional().describe('Absolute path to the local git repo. Defaults to MCP process working directory.'),
|
|
860
1300
|
autoStash: z.boolean().optional().default(false).describe('If working tree is dirty, auto-stash before switching. Labelled with task key.'),
|
|
1301
|
+
agentRole: z.enum(['builder', 'reviewer', 'scout', 'coordinator']).optional()
|
|
1302
|
+
.describe('Set the agent role for this resumed session. Role-specific constraints are injected into cursor rules.'),
|
|
861
1303
|
},
|
|
862
|
-
async ({ taskId, confirmed = false, repoPath, autoStash = false }) => {
|
|
1304
|
+
async ({ taskId, confirmed = false, repoPath, autoStash = false, agentRole }) => {
|
|
863
1305
|
// ── Fetch task ────────────────────────────────────────────────────────
|
|
864
1306
|
let taskRes
|
|
865
1307
|
try { taskRes = await apiWithRetry(() => api.get(`/api/tasks/${taskId}`)) }
|
|
@@ -1017,10 +1459,21 @@ Flow:
|
|
|
1017
1459
|
}
|
|
1018
1460
|
}
|
|
1019
1461
|
|
|
1020
|
-
// Restore cursor rules
|
|
1462
|
+
// Restore cursor rules (with role injection if set)
|
|
1021
1463
|
let cursorRulesFile = null
|
|
1022
|
-
|
|
1023
|
-
|
|
1464
|
+
const hasCursorRulesResume = task?.cursorRules?.trim()
|
|
1465
|
+
if ((hasCursorRulesResume || agentRole) && repoRoot) {
|
|
1466
|
+
cursorRulesFile = writeCursorRulesFile(
|
|
1467
|
+
task.key,
|
|
1468
|
+
hasCursorRulesResume || '(No task-specific rules — follow role constraints above.)',
|
|
1469
|
+
repoPath,
|
|
1470
|
+
agentRole || null
|
|
1471
|
+
)
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
// Persist agentRole to server if set
|
|
1475
|
+
if (agentRole) {
|
|
1476
|
+
api.patch(`/api/tasks/${taskId}`, { agentRole }).catch(() => {/* non-fatal */})
|
|
1024
1477
|
}
|
|
1025
1478
|
|
|
1026
1479
|
// Last commit metadata
|
|
@@ -1028,18 +1481,28 @@ Flow:
|
|
|
1028
1481
|
|
|
1029
1482
|
return text({
|
|
1030
1483
|
resumed: true,
|
|
1031
|
-
task: { key: task.key, title: task.title, column: task.column },
|
|
1484
|
+
task: { key: task.key, title: task.title, column: task.column, agentRole: agentRole || null },
|
|
1032
1485
|
git: gitResult || { switched: false, reason: 'No repo found' },
|
|
1033
1486
|
autoStashed: autoStash && localState === 'modified' && !alreadyOnBranch
|
|
1034
1487
|
? `Changes stashed before switch. Run: git stash list to see them.`
|
|
1035
1488
|
: null,
|
|
1036
1489
|
lastCommit: lastCommit || null,
|
|
1037
1490
|
parkNote: task.parkNote || null,
|
|
1038
|
-
cursorRules: cursorRulesFile
|
|
1491
|
+
cursorRules: cursorRulesFile
|
|
1492
|
+
? { restored: true, path: cursorRulesFile, agentRole: agentRole || null }
|
|
1493
|
+
: { restored: false },
|
|
1039
1494
|
message: gitResult?.switched || gitResult?.alreadyOnBranch
|
|
1040
|
-
? `You are on branch "${branch}". Ready to resume coding.`
|
|
1495
|
+
? `You are on branch "${branch}". Ready to resume coding${agentRole ? ` as ${agentRole}` : ''}.`
|
|
1041
1496
|
: `Branch switch failed — see git.manualSteps.`,
|
|
1042
|
-
nextStep:
|
|
1497
|
+
nextStep: agentRole === 'builder'
|
|
1498
|
+
? `BUILDER role active. Call claim_files to lock your files before editing.`
|
|
1499
|
+
: agentRole === 'scout'
|
|
1500
|
+
? `SCOUT role active. Read-only mode — map the codebase and save findings with update_task(scoutReport=...).`
|
|
1501
|
+
: agentRole === 'coordinator'
|
|
1502
|
+
? `COORDINATOR role active. Call decompose_task to plan parallel workstreams.`
|
|
1503
|
+
: agentRole === 'reviewer'
|
|
1504
|
+
? `REVIEWER role active. Call review_pr to start the review chain.`
|
|
1505
|
+
: task.column === 'in_review'
|
|
1043
1506
|
? `Task is in review. Check if PR feedback needs addressing — call fix_pr_feedback if needed.`
|
|
1044
1507
|
: task.parkNote?.remaining
|
|
1045
1508
|
? `Remaining: ${task.parkNote.remaining}`
|
|
@@ -1047,6 +1510,33 @@ Flow:
|
|
|
1047
1510
|
})
|
|
1048
1511
|
}
|
|
1049
1512
|
)
|
|
1513
|
+
|
|
1514
|
+
// ── log_session_event ─────────────────────────────────────────────────────────
|
|
1515
|
+
server.tool(
|
|
1516
|
+
'log_session_event',
|
|
1517
|
+
`Log a skill invocation, subagent start, rule activation, or informational note to the task's session timeline.
|
|
1518
|
+
Call this whenever you:
|
|
1519
|
+
- Invoke a skill (type="skill", name=skill file name e.g. "scout-codebase")
|
|
1520
|
+
- Spawn a subagent (type="subagent", name=subagent name)
|
|
1521
|
+
- Activate a rule (type="rule", name=rule name)
|
|
1522
|
+
- Want to note a significant step (type="info", name=short label)
|
|
1523
|
+
The log is visible in InternalTool under the task's Session tab — gives the human full visibility into what the agent is doing.`,
|
|
1524
|
+
{
|
|
1525
|
+
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
1526
|
+
type: z.enum(['skill', 'subagent', 'rule', 'info']).describe('Event type'),
|
|
1527
|
+
name: z.string().describe('Name of the skill / subagent / rule / step'),
|
|
1528
|
+
summary: z.string().optional().default('').describe('Optional one-line description of what this event does'),
|
|
1529
|
+
role: z.string().optional().describe('Active agent role at this point (builder, scout, reviewer, coordinator)'),
|
|
1530
|
+
},
|
|
1531
|
+
async ({ taskId, type, name, summary = '', role }) => {
|
|
1532
|
+
trackTaskActivity(taskId, 'log_session_event', { role, summary: `[${type}] ${name}` })
|
|
1533
|
+
// Also push a dedicated entry with the correct event type
|
|
1534
|
+
api.post(`/api/tasks/${taskId}/session/log`, {
|
|
1535
|
+
type, name, role: role || null, summary,
|
|
1536
|
+
}).catch(() => {})
|
|
1537
|
+
return text({ logged: true, type, name, summary })
|
|
1538
|
+
}
|
|
1539
|
+
)
|
|
1050
1540
|
}
|
|
1051
1541
|
|
|
1052
1542
|
// ── Standup activity formatter ────────────────────────────────────────────────
|
|
@@ -1541,8 +2031,11 @@ Use this when a developer says "start task", "brief me on", or "what do I need t
|
|
|
1541
2031
|
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
1542
2032
|
confirmed: z.boolean().optional().default(false).describe('Set true after reading the plan to move the task to in_progress'),
|
|
1543
2033
|
repoPath: z.string().optional().describe('Absolute path to the local git repo (defaults to MCP process working directory). Used to write cursor rules file.'),
|
|
2034
|
+
agentRole: z.enum(['builder', 'reviewer', 'scout', 'coordinator']).optional()
|
|
2035
|
+
.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
2036
|
},
|
|
1545
|
-
async ({ taskId, confirmed = false, repoPath }) => {
|
|
2037
|
+
async ({ taskId, confirmed = false, repoPath, agentRole }) => {
|
|
2038
|
+
trackTaskActivity(taskId, 'kickoff_task')
|
|
1546
2039
|
const taskRes = await api.get(`/api/tasks/${taskId}`)
|
|
1547
2040
|
if (!taskRes?.success) return errorText('Task not found')
|
|
1548
2041
|
const task = taskRes.data.task
|
|
@@ -1573,6 +2066,7 @@ Use this when a developer says "start task", "brief me on", or "what do I need t
|
|
|
1573
2066
|
// ── Preview: show the full plan before touching anything ──
|
|
1574
2067
|
const pendingApv = (task.approvals || []).find(a => a.state === 'pending') || null
|
|
1575
2068
|
const hasApprovedApv = (task.approvals || []).some(a => a.state === 'approved')
|
|
2069
|
+
const approvalState = pendingApv ? 'pending' : hasApprovedApv ? 'approved' : 'none'
|
|
1576
2070
|
const approvalBlocks = ['backlog', 'todo'].includes(task.column) && !hasApprovedApv
|
|
1577
2071
|
|
|
1578
2072
|
// Build the next-step roadmap so developer knows exactly what comes after reading the plan
|
|
@@ -1722,6 +2216,21 @@ Use this when a developer says "start task", "brief me on", or "what do I need t
|
|
|
1722
2216
|
? `⏳ Plan is submitted and awaiting approval — you cannot create a branch until it is approved.`
|
|
1723
2217
|
: `⚠️ Plan is not yet submitted for approval. Submit it first, then create the branch.`
|
|
1724
2218
|
: null,
|
|
2219
|
+
decompositionWarning: (() => {
|
|
2220
|
+
if (!task.decomposition?.trim()) return null
|
|
2221
|
+
try {
|
|
2222
|
+
const dec = JSON.parse(task.decomposition)
|
|
2223
|
+
if (!dec.scoutReportPresent && !task.scoutReport?.trim())
|
|
2224
|
+
return `⚠️ This task has a decomposition plan but no scout report. Consider running scout_task before builders start.`
|
|
2225
|
+
return `Decomposition exists: ${dec.totalSubtasks} subtask(s), ${dec.executionOrder?.length || 0} execution group(s).`
|
|
2226
|
+
} catch { return null }
|
|
2227
|
+
})(),
|
|
2228
|
+
agentContext: {
|
|
2229
|
+
hasScoutReport: !!(task.scoutReport?.trim()),
|
|
2230
|
+
hasDecomposition: !!(task.decomposition?.trim()),
|
|
2231
|
+
claimedFiles: task.claimedFiles || [],
|
|
2232
|
+
agentRole: task.agentRole || null,
|
|
2233
|
+
},
|
|
1725
2234
|
requiresConfirmation: true,
|
|
1726
2235
|
message: approvalBlocks
|
|
1727
2236
|
? `Read the plan above, then follow workflowRoadmap — approval is required before you can branch and start coding.`
|
|
@@ -1757,11 +2266,18 @@ Use this when a developer says "start task", "brief me on", or "what do I need t
|
|
|
1757
2266
|
|
|
1758
2267
|
// ── Confirmed: move to in_progress and fetch recent commits ──
|
|
1759
2268
|
let recentCommits = []
|
|
2269
|
+
let projectAgentConfig = null
|
|
1760
2270
|
try {
|
|
1761
2271
|
const commitsRes = await api.get(`/api/projects/${task.project}/github/commits?per_page=10`)
|
|
1762
2272
|
if (commitsRes?.success) recentCommits = commitsRes.data.commits || []
|
|
1763
2273
|
} catch { /* GitHub may not be linked */ }
|
|
1764
2274
|
|
|
2275
|
+
// Fetch project agentConfig for custom role generation
|
|
2276
|
+
try {
|
|
2277
|
+
const projRes = await api.get(`/api/projects/${task.project}`)
|
|
2278
|
+
if (projRes?.success) projectAgentConfig = projRes.data.project?.agentConfig || null
|
|
2279
|
+
} catch { /* non-fatal */ }
|
|
2280
|
+
|
|
1765
2281
|
// If the task already has a branch linked, it's safe to move to in_progress now.
|
|
1766
2282
|
// If not, create_branch will do the move once the branch is created.
|
|
1767
2283
|
const alreadyHasBranch = !!task.github?.headBranch
|
|
@@ -1773,12 +2289,31 @@ Use this when a developer says "start task", "brief me on", or "what do I need t
|
|
|
1773
2289
|
} catch { /* might already be in_progress */ }
|
|
1774
2290
|
}
|
|
1775
2291
|
|
|
1776
|
-
// Write cursor rules file to local repo immediately on kickoff
|
|
2292
|
+
// Write cursor rules file to local repo immediately on kickoff (with role injection if set)
|
|
1777
2293
|
let cursorRulesFile = null
|
|
1778
2294
|
if (hasCursorRules) {
|
|
1779
|
-
cursorRulesFile = writeCursorRulesFile(task.key, task.cursorRules, repoPath)
|
|
2295
|
+
cursorRulesFile = writeCursorRulesFile(task.key, task.cursorRules, repoPath, agentRole || null)
|
|
2296
|
+
} else if (agentRole) {
|
|
2297
|
+
// Even if no task-specific cursor rules, write role rules alone
|
|
2298
|
+
cursorRulesFile = writeCursorRulesFile(task.key, '(No task-specific rules — follow role constraints above.)', repoPath, agentRole)
|
|
1780
2299
|
}
|
|
1781
2300
|
|
|
2301
|
+
// Dynamically generate .cursor/agents, .cursor/skills, .cursor/commands
|
|
2302
|
+
// based on live task data — so every agent gets the right task ID, files, and role
|
|
2303
|
+
const workspaceResult = writeCursorWorkspace(task, projectAgentConfig, repoPath || process.cwd())
|
|
2304
|
+
|
|
2305
|
+
// Persist agentRole + workspace status to server
|
|
2306
|
+
const workspacePatch = {
|
|
2307
|
+
...(agentRole ? { agentRole } : {}),
|
|
2308
|
+
agentWorkspace: {
|
|
2309
|
+
kickedOffAt: new Date().toISOString(),
|
|
2310
|
+
clearedAt: null,
|
|
2311
|
+
role: agentRole || task.agentRole || null,
|
|
2312
|
+
files: workspaceResult?.written?.map(f => f.replace(workspaceResult.repoRoot, '').replace(/^\//, '')) || [],
|
|
2313
|
+
},
|
|
2314
|
+
}
|
|
2315
|
+
api.patch(`/api/tasks/${taskId}`, workspacePatch).catch(() => {/* non-fatal */})
|
|
2316
|
+
|
|
1782
2317
|
return text({
|
|
1783
2318
|
started: {
|
|
1784
2319
|
key: task.key,
|
|
@@ -1787,14 +2322,26 @@ Use this when a developer says "start task", "brief me on", or "what do I need t
|
|
|
1787
2322
|
column: moved ? 'in_progress' : task.column,
|
|
1788
2323
|
branch: task.github?.headBranch || null,
|
|
1789
2324
|
subtasks,
|
|
2325
|
+
agentRole: agentRole || null,
|
|
1790
2326
|
},
|
|
1791
2327
|
implementationPlan: hasReadme ? task.readmeMarkdown : null,
|
|
1792
|
-
cursorRules: hasCursorRules
|
|
1793
|
-
? {
|
|
2328
|
+
cursorRules: hasCursorRules || agentRole
|
|
2329
|
+
? {
|
|
2330
|
+
active: true,
|
|
2331
|
+
agentRole: agentRole || null,
|
|
2332
|
+
rules: task.cursorRules || null,
|
|
2333
|
+
roleRules: agentRole ? ROLE_RULES[agentRole] : null,
|
|
2334
|
+
instruction: agentRole
|
|
2335
|
+
? `⚠️ AGENT ROLE: ${agentRole.toUpperCase()} — Follow the role behavioral constraints injected into the cursor rules file. These override default behavior.`
|
|
2336
|
+
: '⚠️ CURSOR RULES ACTIVE — You MUST follow every rule in the "rules" field for the entire duration of this task.',
|
|
2337
|
+
}
|
|
1794
2338
|
: { active: false },
|
|
1795
2339
|
cursorRulesFile: cursorRulesFile
|
|
1796
2340
|
? { 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,
|
|
2341
|
+
: (hasCursorRules || agentRole) ? { written: false, note: 'Could not write rules file — not inside a git repo.' } : null,
|
|
2342
|
+
cursorWorkspace: workspaceResult
|
|
2343
|
+
? { written: true, files: workspaceResult.written.map(f => f.replace(workspaceResult.repoRoot, '')), note: 'Agent, skill, and command files generated dynamically from task data. Reload Cursor Settings → Rules, Skills, Subagents to see them.' }
|
|
2344
|
+
: { written: false, note: 'Could not generate workspace files — not inside a git repo.' },
|
|
1798
2345
|
recentCommits: recentCommits.slice(0, 5).map(c => ({
|
|
1799
2346
|
sha: c.sha?.slice(0, 7),
|
|
1800
2347
|
message: c.commit?.message?.split('\n')[0],
|
|
@@ -1803,7 +2350,15 @@ Use this when a developer says "start task", "brief me on", or "what do I need t
|
|
|
1803
2350
|
})),
|
|
1804
2351
|
movedToInProgress: moved,
|
|
1805
2352
|
suggestedBranch: alreadyHasBranch ? null : suggestedBranch,
|
|
1806
|
-
nextStep:
|
|
2353
|
+
nextStep: agentRole === 'scout'
|
|
2354
|
+
? `You are in SCOUT mode. Read the codebase, then save your findings with update_task(scoutReport=...). Do NOT modify any files.`
|
|
2355
|
+
: agentRole === 'coordinator'
|
|
2356
|
+
? `You are in COORDINATOR mode. Read the README, then call decompose_task to break the work into parallel subtasks with file ownership.`
|
|
2357
|
+
: agentRole === 'builder'
|
|
2358
|
+
? `You are in BUILDER mode. Call claim_files first, then start coding on "${alreadyHasBranch ? task.github.headBranch : suggestedBranch}".`
|
|
2359
|
+
: agentRole === 'reviewer'
|
|
2360
|
+
? `You are in REVIEWER mode. Call review_pr to get the diff, then post_pr_review with your analysis.`
|
|
2361
|
+
: alreadyHasBranch
|
|
1807
2362
|
? `Branch "${task.github.headBranch}" already exists. Task is now In progress — start coding.`
|
|
1808
2363
|
: `Call create_branch to create "${suggestedBranch}" — it will check your local git state and move the task to In progress automatically.`,
|
|
1809
2364
|
})
|
|
@@ -2210,15 +2765,118 @@ function findRepoRoot(startPath) {
|
|
|
2210
2765
|
return null
|
|
2211
2766
|
}
|
|
2212
2767
|
|
|
2213
|
-
|
|
2214
|
-
|
|
2768
|
+
// ── Agent role behavioral rule templates ─────────────────────────────────────
|
|
2769
|
+
// Prepended to cursor rules when an agentRole is set. Defines what the agent
|
|
2770
|
+
// CAN and CANNOT do for the duration of the task session.
|
|
2771
|
+
const ROLE_RULES = {
|
|
2772
|
+
builder: `## Agent Role: BUILDER
|
|
2773
|
+
|
|
2774
|
+
You are a BUILDER agent. Your behavioral constraints for this session:
|
|
2775
|
+
|
|
2776
|
+
**ALLOWED:**
|
|
2777
|
+
- Write, modify, and delete code files within the task scope
|
|
2778
|
+
- Create new files required by the implementation plan
|
|
2779
|
+
- Commit and push changes on the task branch
|
|
2780
|
+
- Run tests and fix failures
|
|
2781
|
+
- Claim file ownership before editing (use claim_files MCP tool)
|
|
2782
|
+
|
|
2783
|
+
**NOT ALLOWED:**
|
|
2784
|
+
- Modify files owned by another in-progress task (check claimedFiles conflicts)
|
|
2785
|
+
- Make architectural decisions not in the implementation plan — flag them instead
|
|
2786
|
+
- Edit files outside the task scope without explicit approval
|
|
2787
|
+
- Merge to main/master/dev directly
|
|
2788
|
+
|
|
2789
|
+
**WORK STYLE:**
|
|
2790
|
+
- Read the implementation plan fully before writing any code
|
|
2791
|
+
- Claim your files at the start with claim_files
|
|
2792
|
+
- Follow the spec precisely — don't add unrequested features
|
|
2793
|
+
- Commit atomically with conventional commit format (feat/fix/refactor)`,
|
|
2794
|
+
|
|
2795
|
+
scout: `## Agent Role: SCOUT
|
|
2796
|
+
|
|
2797
|
+
You are a SCOUT agent. Your behavioral constraints for this session:
|
|
2798
|
+
|
|
2799
|
+
**ALLOWED:**
|
|
2800
|
+
- Read and analyze any file in the codebase
|
|
2801
|
+
- Search for patterns, dependencies, and potential conflicts
|
|
2802
|
+
- Write a structured scout report (update_task with scoutReport field)
|
|
2803
|
+
- Identify which files the implementation will need to touch
|
|
2804
|
+
- Surface risks, gotchas, and architectural considerations
|
|
2805
|
+
|
|
2806
|
+
**NOT ALLOWED:**
|
|
2807
|
+
- Modify any source code files
|
|
2808
|
+
- Create branches or commits
|
|
2809
|
+
- Move tasks or create PRs
|
|
2810
|
+
- Make changes that would require a PR
|
|
2811
|
+
|
|
2812
|
+
**WORK STYLE:**
|
|
2813
|
+
- Your output is a scout report for the builder agents
|
|
2814
|
+
- Map file dependencies before builders start
|
|
2815
|
+
- Identify conflict risks with other in-progress tasks
|
|
2816
|
+
- Estimate complexity and flag potential blockers
|
|
2817
|
+
- Save your report with update_task(scoutReport=...)`,
|
|
2818
|
+
|
|
2819
|
+
reviewer: `## Agent Role: REVIEWER
|
|
2820
|
+
|
|
2821
|
+
You are a REVIEWER agent. Your behavioral constraints for this session:
|
|
2822
|
+
|
|
2823
|
+
**ALLOWED:**
|
|
2824
|
+
- Read all code changes and diffs (use review_pr)
|
|
2825
|
+
- Post structured reviews with specific analysis points (use post_pr_review)
|
|
2826
|
+
- Request changes or approve the PR
|
|
2827
|
+
- Reference specific files, functions, and line numbers in your review
|
|
2828
|
+
|
|
2829
|
+
**NOT ALLOWED:**
|
|
2830
|
+
- Approve a PR without reading every file in the diff
|
|
2831
|
+
- Write "looks good" or generic analysis — be specific and file-based
|
|
2832
|
+
- Merge the PR without completing the review chain (review_pr → post_pr_review → merge_pr)
|
|
2833
|
+
- Modify source code directly
|
|
2834
|
+
|
|
2835
|
+
**WORK STYLE:**
|
|
2836
|
+
- Always call review_pr first to get the full diff and spec
|
|
2837
|
+
- Check each file against the implementation plan
|
|
2838
|
+
- Build analysisPoints with file-specific findings (minimum 2)
|
|
2839
|
+
- Reference function names and line numbers
|
|
2840
|
+
- Only call merge_pr after post_pr_review returns a reviewId`,
|
|
2841
|
+
|
|
2842
|
+
coordinator: `## Agent Role: COORDINATOR
|
|
2843
|
+
|
|
2844
|
+
You are a COORDINATOR agent. Your behavioral constraints for this session:
|
|
2845
|
+
|
|
2846
|
+
**ALLOWED:**
|
|
2847
|
+
- Read the task implementation plan and codebase structure
|
|
2848
|
+
- Decompose the plan into parallel subtasks (use decompose_task)
|
|
2849
|
+
- Assign file ownership to each subtask with no overlaps
|
|
2850
|
+
- Assign roles to each subtask (builder, scout, reviewer)
|
|
2851
|
+
- Create subtasks on the board (use update_task with subtasks)
|
|
2852
|
+
- Monitor progress and resolve file conflicts
|
|
2853
|
+
|
|
2854
|
+
**NOT ALLOWED:**
|
|
2855
|
+
- Write implementation code directly
|
|
2856
|
+
- Claim file ownership for yourself
|
|
2857
|
+
- Bypass the decomposition step — always decompose before builders start
|
|
2858
|
+
- Start coding without a scout report when the codebase is unfamiliar
|
|
2859
|
+
|
|
2860
|
+
**WORK STYLE:**
|
|
2861
|
+
- Start with decompose_task to get the structured execution plan
|
|
2862
|
+
- Ensure no two builder subtasks claim the same file
|
|
2863
|
+
- Scout report must exist before builders begin
|
|
2864
|
+
- Each builder subtask must have explicit file ownership and a clear scope`,
|
|
2865
|
+
}
|
|
2866
|
+
|
|
2867
|
+
/** Write task-specific cursor rules to .cursor/rules/<taskKey>.mdc in the local repo root.
|
|
2868
|
+
* When role is provided, role-specific behavioral constraints are prepended. */
|
|
2869
|
+
function writeCursorRulesFile(taskKey, rulesMarkdown, startPath, role = null) {
|
|
2215
2870
|
try {
|
|
2216
2871
|
const repoRoot = findRepoRoot(startPath)
|
|
2217
2872
|
if (!repoRoot) return null
|
|
2218
2873
|
const rulesDir = join(repoRoot, '.cursor', 'rules')
|
|
2219
2874
|
mkdirSync(rulesDir, { recursive: true })
|
|
2220
2875
|
const filePath = join(rulesDir, `${taskKey.toLowerCase()}.mdc`)
|
|
2221
|
-
const
|
|
2876
|
+
const roleSection = role && ROLE_RULES[role]
|
|
2877
|
+
? `${ROLE_RULES[role]}\n\n---\n\n## Task-Specific Rules\n\n`
|
|
2878
|
+
: ''
|
|
2879
|
+
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
2880
|
writeFileSync(filePath, content, 'utf8')
|
|
2223
2881
|
return filePath
|
|
2224
2882
|
} catch {
|
|
@@ -2226,6 +2884,202 @@ function writeCursorRulesFile(taskKey, rulesMarkdown, startPath) {
|
|
|
2226
2884
|
}
|
|
2227
2885
|
}
|
|
2228
2886
|
|
|
2887
|
+
/**
|
|
2888
|
+
* Dynamically generate .cursor/ workspace files from live InternalTool data.
|
|
2889
|
+
*
|
|
2890
|
+
* Sources (in priority order):
|
|
2891
|
+
* 1. project.agentConfig.subagents[] → .cursor/agents/<name>.md
|
|
2892
|
+
* 2. project.agentConfig.skills[] → .cursor/skills/<name>.md
|
|
2893
|
+
* 3. project.agentConfig.rules[] → .cursor/rules/<name>.mdc (project-level, alwaysApply)
|
|
2894
|
+
* 4. Built-in ROLE_RULES / default skill templates (fallback when DB arrays are empty)
|
|
2895
|
+
*
|
|
2896
|
+
* Task-specific agent file (.cursor/agents/active-agent.md) is always written
|
|
2897
|
+
* from the task's role + claimed files + task ID so Cursor knows exactly what to do.
|
|
2898
|
+
* It is deleted by deleteCursorWorkspace() when the task is parked or completed.
|
|
2899
|
+
*/
|
|
2900
|
+
function writeCursorWorkspace(task, projectAgentConfig, startPath) {
|
|
2901
|
+
try {
|
|
2902
|
+
const repoRoot = findRepoRoot(startPath)
|
|
2903
|
+
if (!repoRoot) return null
|
|
2904
|
+
|
|
2905
|
+
const role = task.agentRole || null
|
|
2906
|
+
const taskId = String(task._id)
|
|
2907
|
+
const taskKey = task.key || 'TASK-???'
|
|
2908
|
+
const taskTitle = task.title || ''
|
|
2909
|
+
const claimedFiles = task.claimedFiles || []
|
|
2910
|
+
const hasScout = !!(task.scoutReport?.trim())
|
|
2911
|
+
const hasDec = !!(task.decomposition?.trim())
|
|
2912
|
+
const cfg = projectAgentConfig || {}
|
|
2913
|
+
|
|
2914
|
+
const agentsDir = join(repoRoot, '.cursor', 'agents')
|
|
2915
|
+
const skillsDir = join(repoRoot, '.cursor', 'skills')
|
|
2916
|
+
const commandsDir = join(repoRoot, '.cursor', 'commands')
|
|
2917
|
+
const rulesDir = join(repoRoot, '.cursor', 'rules')
|
|
2918
|
+
mkdirSync(agentsDir, { recursive: true })
|
|
2919
|
+
mkdirSync(skillsDir, { recursive: true })
|
|
2920
|
+
mkdirSync(commandsDir, { recursive: true })
|
|
2921
|
+
mkdirSync(rulesDir, { recursive: true })
|
|
2922
|
+
|
|
2923
|
+
const written = []
|
|
2924
|
+
|
|
2925
|
+
// ── 1. Project-level rules from DB → .cursor/rules/<name>.mdc ─────────────
|
|
2926
|
+
const projectRules = cfg.rules || []
|
|
2927
|
+
for (const r of projectRules) {
|
|
2928
|
+
if (!r.name || !r.body) continue
|
|
2929
|
+
const content = `---\ndescription: ${r.description || r.name} — project rule managed in InternalTool. Do not edit manually.\nalwaysApply: ${r.alwaysApply ? 'true' : 'false'}\n---\n\n${r.body}\n`
|
|
2930
|
+
const f = join(rulesDir, `${r.name}.mdc`)
|
|
2931
|
+
writeFileSync(f, content, 'utf8')
|
|
2932
|
+
written.push(f)
|
|
2933
|
+
}
|
|
2934
|
+
|
|
2935
|
+
// ── 2. Subagents from DB → .cursor/agents/<name>.md ───────────────────────
|
|
2936
|
+
// If DB has subagents defined, write them verbatim (admin-authored markdown)
|
|
2937
|
+
const dbSubagents = cfg.subagents || []
|
|
2938
|
+
for (const s of dbSubagents) {
|
|
2939
|
+
if (!s.name || !s.body) continue
|
|
2940
|
+
const header = `---\nname: ${s.name}\ndescription: ${s.description || s.name}\n---\n\n`
|
|
2941
|
+
const f = join(agentsDir, `${s.name}.md`)
|
|
2942
|
+
writeFileSync(f, header + s.body, 'utf8')
|
|
2943
|
+
written.push(f)
|
|
2944
|
+
}
|
|
2945
|
+
|
|
2946
|
+
// ── 3. Task-specific active-agent.md — always written, deleted on done ────
|
|
2947
|
+
// Uses DB subagent body as template if one matches the role; falls back to
|
|
2948
|
+
// built-in ROLE_RULES template with task-specific data injected.
|
|
2949
|
+
if (role) {
|
|
2950
|
+
const dbMatch = dbSubagents.find(s => s.name === `${role}-agent` || s.name === role)
|
|
2951
|
+
let agentBody
|
|
2952
|
+
if (dbMatch?.body) {
|
|
2953
|
+
// Admin wrote a custom body — inject task data into placeholder tokens
|
|
2954
|
+
agentBody = dbMatch.body
|
|
2955
|
+
.replace(/\{\{taskId\}\}/g, taskId)
|
|
2956
|
+
.replace(/\{\{taskKey\}\}/g, taskKey)
|
|
2957
|
+
.replace(/\{\{taskTitle\}\}/g, taskTitle)
|
|
2958
|
+
.replace(/\{\{claimedFiles\}\}/g, claimedFiles.map(f => `- \`${f}\``).join('\n') || '(none)')
|
|
2959
|
+
} else {
|
|
2960
|
+
// Built-in fallback template
|
|
2961
|
+
const fileLine = claimedFiles.length
|
|
2962
|
+
? claimedFiles.map(f => `- \`${f}\``).join('\n')
|
|
2963
|
+
: '- (no files claimed yet — call claim_files first)'
|
|
2964
|
+
const scoutLine = hasScout ? '✅ Scout report available — call `get_agent_context` to read it.' : '⚠️ No scout report yet.'
|
|
2965
|
+
const decLine = hasDec ? '✅ Execution plan exists — follow the decomposition order.' : ''
|
|
2966
|
+
|
|
2967
|
+
const BUILT_IN = {
|
|
2968
|
+
scout: `# Scout Agent — ${taskKey}\n\n**Task ID:** \`${taskId}\`\n**Task:** ${taskTitle}\n\n## Constraints\n- READ ONLY — do not write or modify any file\n- You CAN read files, run tests, and use MCP tools\n\n## Workflow\n1. Call \`get_agent_context\` with taskId \`${taskId}\`\n2. Read every source file systematically\n3. Call \`scout_task\` with confirmed=false, then confirmed=true + your report\n`,
|
|
2969
|
+
builder: `# Builder Agent — ${taskKey}\n\n**Task ID:** \`${taskId}\`\n**Task:** ${taskTitle}\n\n**Claimed files:**\n${fileLine}\n\n${scoutLine}\n${decLine}\n\n## Constraints\n- Only modify your claimed files\n- Every change needs a test update\n- Run tests before marking done\n\n## Workflow\n1. Call \`get_agent_context\` with taskId \`${taskId}\`\n2. Implement the feature\n3. Use the \`run-tests\` skill to verify\n4. Call \`commit_helper\` then \`raise_pr\`\n`,
|
|
2970
|
+
coordinator: `# Coordinator Agent — ${taskKey}\n\n**Task ID:** \`${taskId}\`\n**Task:** ${taskTitle}\n\n## Constraints\n- Do NOT write code — plan and delegate only\n- Each subtask must have exclusive file ownership\n\n## Workflow\n1. Call \`get_agent_context\` with taskId \`${taskId}\`\n2. Break work into subtasks with role + files + description\n3. Call \`decompose_task\` with taskId \`${taskId}\` to save the plan\n`,
|
|
2971
|
+
reviewer: `# Reviewer Agent — ${taskKey}\n\n**Task ID:** \`${taskId}\`\n**Task:** ${taskTitle}\n\n## Constraints\n- Read the full diff before approving\n- Post specific, file-level comments\n\n## Workflow\n1. Call \`get_agent_context\` with taskId \`${taskId}\`\n2. Call \`review_pr\` to fetch the diff\n3. Call \`post_pr_review\` with your verdict\n`,
|
|
2972
|
+
}
|
|
2973
|
+
agentBody = BUILT_IN[role] || `# ${role} Agent — ${taskKey}\n\n**Task ID:** \`${taskId}\`\n**Task:** ${taskTitle}\n`
|
|
2974
|
+
}
|
|
2975
|
+
|
|
2976
|
+
const activeAgentContent = `---\nname: active-agent\ndescription: ${role} agent for ${taskKey} — auto-generated by InternalTool MCP on kickoff. Deleted when task is parked or done.\n---\n\n${agentBody}`
|
|
2977
|
+
writeFileSync(join(agentsDir, 'active-agent.md'), activeAgentContent, 'utf8')
|
|
2978
|
+
written.push(join(agentsDir, 'active-agent.md'))
|
|
2979
|
+
}
|
|
2980
|
+
|
|
2981
|
+
// ── 4. Custom role agents from project.agentConfig.roles (non-subagent) ───
|
|
2982
|
+
for (const cr of (cfg.roles || [])) {
|
|
2983
|
+
if (!cr.name || dbSubagents.some(s => s.name === `${cr.name}-agent`)) continue
|
|
2984
|
+
const content = `---\nname: ${cr.name}-agent\ndescription: ${cr.label || cr.name}${cr.promptHint ? ' — ' + cr.promptHint.slice(0, 80) : ''}\n---\n\n# ${cr.label || cr.name} Agent\n\n**Role:** \`${cr.name}\`\n${cr.promptHint ? `\n## Instructions\n${cr.promptHint}\n` : ''}${cr.allowedTools?.length ? `\n## Allowed tools\n${cr.allowedTools.map(t => `- ${t}`).join('\n')}\n` : ''}`
|
|
2985
|
+
const f = join(agentsDir, `${cr.name}-agent.md`)
|
|
2986
|
+
writeFileSync(f, content, 'utf8')
|
|
2987
|
+
written.push(f)
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2990
|
+
// ── 5. Skills from DB → .cursor/skills/<name>.md ──────────────────────────
|
|
2991
|
+
// If DB is empty, write built-in defaults so Cursor always has something useful.
|
|
2992
|
+
const dbSkills = cfg.skills || []
|
|
2993
|
+
if (dbSkills.length > 0) {
|
|
2994
|
+
for (const s of dbSkills) {
|
|
2995
|
+
if (!s.name || !s.body) continue
|
|
2996
|
+
const header = `---\nname: ${s.name}\ndescription: ${s.description || s.name}\n---\n\n`
|
|
2997
|
+
const f = join(skillsDir, `${s.name}.md`)
|
|
2998
|
+
writeFileSync(f, header + s.body, 'utf8')
|
|
2999
|
+
written.push(f)
|
|
3000
|
+
}
|
|
3001
|
+
} else {
|
|
3002
|
+
// Built-in default skills
|
|
3003
|
+
const defaults = [
|
|
3004
|
+
{
|
|
3005
|
+
name: 'scout-codebase',
|
|
3006
|
+
description: 'Read every source file and save a scout report to InternalTool.',
|
|
3007
|
+
body: `1. Read all source files: routes, models, middleware, utils, tests, README, .cursorrules\n2. Write a report: Auth Flow, Data Model, API Surface, Utilities, Test Coverage, Gaps & Risks\n3. Call \`scout_task\` with taskId \`${taskId}\` confirmed=false (preview), then confirmed=true + report\n\n**READ ONLY — do not modify any file.**`,
|
|
3008
|
+
},
|
|
3009
|
+
{
|
|
3010
|
+
name: 'run-tests',
|
|
3011
|
+
description: 'Run the test suite and fix any failures before marking done.',
|
|
3012
|
+
body: `1. Run \`npm test\`\n2. Report: passed / failed / skipped\n3. For each failure: identify root cause, fix source (not the test)\n4. Re-run until 0 failures\n5. Do NOT mark done until tests are green`,
|
|
3013
|
+
},
|
|
3014
|
+
]
|
|
3015
|
+
for (const s of defaults) {
|
|
3016
|
+
const content = `---\nname: ${s.name}\ndescription: ${s.description}\n---\n\n${s.body}\n`
|
|
3017
|
+
const f = join(skillsDir, `${s.name}.md`)
|
|
3018
|
+
writeFileSync(f, content, 'utf8')
|
|
3019
|
+
written.push(f)
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
|
|
3023
|
+
// ── 6. Command — start-task pre-filled with this task ─────────────────────
|
|
3024
|
+
const startCmd = `---\nname: start-task\ndescription: Start a task from InternalTool — brief, role rules, moves to In Progress.\n---\n\nDefault task: \`${taskId}\` (${taskKey} — ${taskTitle})\n\n1. Ask the user to confirm the task ID or use the default above\n2. Call \`kickoff_task\` with confirmed=false to show the brief\n3. Present: role, description, claimedFiles, cursorRules\n4. Ask "Ready to confirm and move to In Progress?"\n5. If yes, call \`kickoff_task\` again with confirmed=true\n`
|
|
3025
|
+
writeFileSync(join(commandsDir, 'start-task.md'), startCmd, 'utf8')
|
|
3026
|
+
written.push(join(commandsDir, 'start-task.md'))
|
|
3027
|
+
|
|
3028
|
+
return { repoRoot, written }
|
|
3029
|
+
} catch {
|
|
3030
|
+
return null
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
|
|
3034
|
+
/**
|
|
3035
|
+
* Delete task-specific workspace files written at kickoff.
|
|
3036
|
+
* Project-level rules (.cursor/rules/<project-rule>.mdc) and skills are kept.
|
|
3037
|
+
* Only the active-agent and start-task command are removed (they are task-scoped).
|
|
3038
|
+
*/
|
|
3039
|
+
function deleteCursorWorkspace(role, startPath) {
|
|
3040
|
+
const deleted = []
|
|
3041
|
+
try {
|
|
3042
|
+
const repoRoot = findRepoRoot(startPath)
|
|
3043
|
+
if (!repoRoot) return deleted
|
|
3044
|
+
const toDelete = [
|
|
3045
|
+
join(repoRoot, '.cursor', 'agents', 'active-agent.md'),
|
|
3046
|
+
join(repoRoot, '.cursor', 'commands', 'start-task.md'),
|
|
3047
|
+
]
|
|
3048
|
+
for (const f of toDelete) {
|
|
3049
|
+
try {
|
|
3050
|
+
if (existsSync(f)) { unlinkSync(f); deleted.push(f) }
|
|
3051
|
+
} catch { /* non-fatal */ }
|
|
3052
|
+
}
|
|
3053
|
+
} catch { /* non-fatal */ }
|
|
3054
|
+
return deleted
|
|
3055
|
+
}
|
|
3056
|
+
|
|
3057
|
+
/**
|
|
3058
|
+
* Record that a specific MCP tool was called for a task.
|
|
3059
|
+
* 1. Updates agentWorkspace.lastActivityAt + lastTool (fast summary for overview card)
|
|
3060
|
+
* 2. Appends to agentSessionLog (the ordered call history shown in Session tab)
|
|
3061
|
+
* Fire-and-forget: never blocks the tool response.
|
|
3062
|
+
*/
|
|
3063
|
+
function trackTaskActivity(taskId, toolName, opts = {}) {
|
|
3064
|
+
if (!taskId) return
|
|
3065
|
+
// Fast summary update
|
|
3066
|
+
api.patch(`/api/tasks/${taskId}`, {
|
|
3067
|
+
agentWorkspace: {
|
|
3068
|
+
lastActivityAt: new Date().toISOString(),
|
|
3069
|
+
lastTool: toolName,
|
|
3070
|
+
_incCallCount: true,
|
|
3071
|
+
...(opts.role ? { role: opts.role } : {}),
|
|
3072
|
+
},
|
|
3073
|
+
}).catch(() => {})
|
|
3074
|
+
// Ordered session log entry
|
|
3075
|
+
api.post(`/api/tasks/${taskId}/session/log`, {
|
|
3076
|
+
type: 'tool',
|
|
3077
|
+
name: toolName,
|
|
3078
|
+
role: opts.role || null,
|
|
3079
|
+
summary: opts.summary || '',
|
|
3080
|
+
}).catch(() => {})
|
|
3081
|
+
}
|
|
3082
|
+
|
|
2229
3083
|
/** Delete the task-specific cursor rules file when work is complete. */
|
|
2230
3084
|
function deleteCursorRulesFile(taskKey, startPath) {
|
|
2231
3085
|
try {
|
|
@@ -2680,6 +3534,7 @@ Use this when returning to a task after a break, or when Claude needs the full p
|
|
|
2680
3534
|
repoPath: z.string().optional().describe('Absolute path to the local git repo (defaults to MCP process working directory)'),
|
|
2681
3535
|
},
|
|
2682
3536
|
async ({ taskId, repoPath }) => {
|
|
3537
|
+
trackTaskActivity(taskId, 'get_task_context')
|
|
2683
3538
|
let taskRes, activityRes
|
|
2684
3539
|
try {
|
|
2685
3540
|
[taskRes, activityRes] = await Promise.all([
|
|
@@ -2816,7 +3671,9 @@ If you have uncommitted tracked changes, it will tell you exactly what to do bef
|
|
|
2816
3671
|
// ── Approval gate check ───────────────────────────────────────────────────
|
|
2817
3672
|
// The server blocks non-admins from moving todo → in_progress without approval.
|
|
2818
3673
|
// Detect this early and guide the developer instead of failing silently after branch creation.
|
|
3674
|
+
const pendingApv2 = (task.approvals || []).find(a => a.state === 'pending') || null
|
|
2819
3675
|
const hasApprovedApv2 = (task.approvals || []).some(a => a.state === 'approved')
|
|
3676
|
+
const approvalState = pendingApv2 ? 'pending' : hasApprovedApv2 ? 'approved' : 'none'
|
|
2820
3677
|
const PLANNING_COLS = ['backlog', 'todo']
|
|
2821
3678
|
const needsApproval = PLANNING_COLS.includes(task.column) && !hasApprovedApv2
|
|
2822
3679
|
if (needsApproval && !confirmed) {
|
package/package.json
CHANGED