internaltool-mcp 1.6.42 → 1.6.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +1060 -254
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -520,73 +520,69 @@ After approval, the skill/rule is injected at every future kickoff_task.`,
|
|
|
520
520
|
// ── plan_task_from_codebase ───────────────────────────────────────────────────
|
|
521
521
|
server.tool(
|
|
522
522
|
'plan_task_from_codebase',
|
|
523
|
-
`Specialized task-creation agent —
|
|
524
|
-
|
|
525
|
-
Use this instead of create_task when you need to implement something and want the task to
|
|
526
|
-
contain a real implementation plan based on the actual codebase, not a generic description.
|
|
523
|
+
`Specialized task-creation agent — scouts the codebase, sets up the agent config, and creates a fully-planned, kickoff-ready task.
|
|
527
524
|
|
|
528
525
|
## MANDATORY protocol — follow every step in order
|
|
529
526
|
|
|
530
|
-
### Step 1 — Duplicate check
|
|
527
|
+
### Step 1 — Duplicate check
|
|
531
528
|
Call search_tasks(projectId, query="<keywords from the request>").
|
|
532
|
-
If a similar open task
|
|
529
|
+
If a similar open task exists → return it with kickoff instructions. Do NOT create a duplicate.
|
|
533
530
|
|
|
534
|
-
### Step 2 — Codebase
|
|
531
|
+
### Step 2 — Codebase scout (READ the code — do not guess)
|
|
535
532
|
Using your native Read / Grep / Glob tools:
|
|
536
533
|
|
|
537
534
|
a) **Stack detection** — read package.json / go.mod / requirements.txt / Cargo.toml.
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
535
|
+
b) **Entry point mapping** — grep for route/component/schema patterns relevant to the feature.
|
|
536
|
+
c) **Pattern extraction** — read 2–3 files similar to what you'll build. Note naming conventions, folder structure, wiring patterns.
|
|
537
|
+
d) **Impact analysis** — list every file to CREATE and every file to MODIFY.
|
|
538
|
+
e) **Dependency order** — schema → model → service → route → test → UI.
|
|
539
|
+
|
|
540
|
+
### Step 3 — Agent config analysis (use agentConfigAnalysis returned by this tool)
|
|
541
|
+
The tool returns agentConfigAnalysis showing what the project already has and what it needs.
|
|
542
|
+
Act on it BEFORE creating the task:
|
|
543
|
+
|
|
544
|
+
**a) suggest_skill for each item in agentConfigAnalysis.suggestForProject:**
|
|
545
|
+
Call suggest_skill(projectId, type, name, description, body, rationale, sourceTaskKey)
|
|
546
|
+
— one call per missing skill/rule. Use the body template from agentConfigAnalysis.suggestBodyTemplate.
|
|
547
|
+
These go to the admin approval queue and will be injected at every future kickoff.
|
|
548
|
+
|
|
549
|
+
**b) Set task-level kit via agentKitOverrides in create_task:**
|
|
550
|
+
Pass agentKitOverrides = agentConfigAnalysis.taskKitOverrides
|
|
551
|
+
— this pre-selects the right agents/skills/prompts for THIS task without touching project-level config.
|
|
552
|
+
|
|
553
|
+
**c) Use remember() for facts that cannot be expressed as a skill or rule:**
|
|
554
|
+
After create_task, call remember(taskId, key, value) for each fact in agentConfigAnalysis.rememberFacts.
|
|
555
|
+
Examples: "auth_module = server/middleware/auth.js", "test_runner = jest --runInBand"
|
|
556
|
+
|
|
557
|
+
### Step 4 — Write the implementation plan
|
|
558
|
+
Using findings from Step 2:
|
|
556
559
|
- ## Goal — one sentence
|
|
557
|
-
- ## Stack —
|
|
558
|
-
- ## Technical approach —
|
|
559
|
-
- ## Files to create
|
|
560
|
-
- ## Files to modify
|
|
561
|
-
- ## Subtasks — ordered
|
|
562
|
-
- ## Acceptance criteria
|
|
563
|
-
|
|
564
|
-
### Step 4 — Determine task metadata
|
|
565
|
-
- taskType: feature / bugfix / migration / integration / ui / backend / security / refactor
|
|
566
|
-
- priority: low / medium / high / critical (use "high" if the request sounds important)
|
|
567
|
-
- suggestedFiles: the exact file paths from your Step 2d impact analysis
|
|
560
|
+
- ## Stack — detected stack
|
|
561
|
+
- ## Technical approach — real file names and functions
|
|
562
|
+
- ## Files to create
|
|
563
|
+
- ## Files to modify
|
|
564
|
+
- ## Subtasks — ordered steps
|
|
565
|
+
- ## Acceptance criteria
|
|
568
566
|
|
|
569
567
|
### Step 5 — Create the task
|
|
570
568
|
Call create_task with:
|
|
571
|
-
- projectId (
|
|
572
|
-
-
|
|
573
|
-
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
-
|
|
584
|
-
-
|
|
585
|
-
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
Do NOT skip the codebase analysis. Do NOT create the task before reading the code.
|
|
589
|
-
Do NOT ask the developer to describe the codebase — read it yourself.`,
|
|
569
|
+
- projectId, title (verb+noun), description, readmeMarkdown, taskType, priority, column="todo"
|
|
570
|
+
- subtasks (ordered), suggestedFiles (from Step 2d)
|
|
571
|
+
- agentKitOverrides from Step 3b
|
|
572
|
+
|
|
573
|
+
### Step 6 — Post-creation setup
|
|
574
|
+
1. For each fact in agentConfigAnalysis.rememberFacts: call remember(taskId, key, value)
|
|
575
|
+
2. Log to session: call log_session_event(taskId, type="info", name="task-created", summary="Scouted codebase, configured kit, created task")
|
|
576
|
+
3. Present the approval summary to the developer and wait for submit_task_for_approval confirmation
|
|
577
|
+
|
|
578
|
+
## Quality checklist
|
|
579
|
+
- readmeMarkdown references REAL file paths
|
|
580
|
+
- subtasks are ordered (schema → model → service → route → test → UI)
|
|
581
|
+
- agentKitOverrides is set based on task type
|
|
582
|
+
- suggest_skill was called for any missing project-level skills
|
|
583
|
+
- remember() was called for codebase-specific facts
|
|
584
|
+
|
|
585
|
+
Do NOT skip the scout phase. Do NOT ask the developer to describe the codebase.`,
|
|
590
586
|
{
|
|
591
587
|
projectId: z.string().describe("InternalTool project's MongoDB ObjectId — from the project's task board URL or CLAUDE.md"),
|
|
592
588
|
request: z.string().describe('What the developer wants to build — the raw natural language request (e.g. "add rate limiting to the login endpoint", "fix the pagination bug on users list")'),
|
|
@@ -604,8 +600,9 @@ Do NOT ask the developer to describe the codebase — read it yourself.`,
|
|
|
604
600
|
const dirTree = getDirTree(cwd, 2)
|
|
605
601
|
const entryPoints = findEntryPoints(cwd, stack)
|
|
606
602
|
|
|
607
|
-
// ── 2. Fetch project context
|
|
603
|
+
// ── 2. Fetch project context + existing agentConfig ─────────────────────
|
|
608
604
|
let projectContext = null
|
|
605
|
+
let existingAgentConfig = { skills: [], rules: [], subagents: [], prompts: [] }
|
|
609
606
|
try {
|
|
610
607
|
const projRes = await api.get(`/api/projects/${projectId}`)
|
|
611
608
|
if (projRes?.success) {
|
|
@@ -615,6 +612,13 @@ Do NOT ask the developer to describe the codebase — read it yourself.`,
|
|
|
615
612
|
taskCount: (projRes.data.tasks || []).length,
|
|
616
613
|
githubRepo: p.github?.repoUrl || null,
|
|
617
614
|
}
|
|
615
|
+
const cfg = p.agentConfig || {}
|
|
616
|
+
existingAgentConfig = {
|
|
617
|
+
skills: (cfg.skills || []).map(s => s.name),
|
|
618
|
+
rules: (cfg.rules || []).map(r => r.name),
|
|
619
|
+
subagents: (cfg.subagents || []).map(a => a.name),
|
|
620
|
+
prompts: (cfg.prompts || []).map(p => p.name),
|
|
621
|
+
}
|
|
618
622
|
}
|
|
619
623
|
} catch { /* non-fatal */ }
|
|
620
624
|
|
|
@@ -632,7 +636,102 @@ Do NOT ask the developer to describe the codebase — read it yourself.`,
|
|
|
632
636
|
}
|
|
633
637
|
} catch { /* non-fatal */ }
|
|
634
638
|
|
|
635
|
-
// ── 4.
|
|
639
|
+
// ── 4. Agent config gap analysis ─────────────────────────────────────────
|
|
640
|
+
// Determine what kit the task needs based on stack + request keywords
|
|
641
|
+
const requestCorpus = `${request} ${stack.language || ''} ${stack.framework || ''} ${(stack.extra || []).join(' ')}`.toLowerCase()
|
|
642
|
+
|
|
643
|
+
// Map task type keywords → what kit category is relevant
|
|
644
|
+
const KIT_CATEGORY_HINTS = {
|
|
645
|
+
security: ['auth', 'security', 'jwt', 'password', 'permission', 'rbac', 'owasp', 'token', 'encrypt', 'rate limit', 'sanitize'],
|
|
646
|
+
scout: ['explore', 'understand', 'architecture', 'trace', 'how does', 'what is', 'map', 'audit', 'recon', 'investigate'],
|
|
647
|
+
merge: ['merge', 'rebase', 'conflict', 'ship', 'push', 'pr', 'branch'],
|
|
648
|
+
review: ['review', 'code review', 'diff', 'approve', 'feedback', 'pull request'],
|
|
649
|
+
coordinator: ['large', 'complex', 'parallel', 'coordinate', 'orchestrate', 'decompose', 'multi-step'],
|
|
650
|
+
builder: ['build', 'implement', 'create', 'add', 'fix', 'feature', 'endpoint', 'route', 'component', 'model', 'service', 'crud'],
|
|
651
|
+
}
|
|
652
|
+
let detectedKitCategory = 'builder'
|
|
653
|
+
let maxKitScore = 0
|
|
654
|
+
for (const [cat, keywords] of Object.entries(KIT_CATEGORY_HINTS)) {
|
|
655
|
+
const score = keywords.filter(kw => requestCorpus.includes(kw)).length
|
|
656
|
+
if (score > maxKitScore) { maxKitScore = score; detectedKitCategory = cat }
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Kit catalog — what skills/agents/prompts exist in ZopKit
|
|
660
|
+
const ZOPKIT_SKILLS_CATALOG = ['zop-blueprint', 'zop-recon', 'zop-guard-skill', 'zop-merge-skill', 'zop-review-skill', 'session-start']
|
|
661
|
+
const ZOPKIT_AGENTS_CATALOG = ['zop-shipper', 'zop-scout', 'zop-guard', 'zop-merger', 'zop-reviewer', 'zop-orchestrator']
|
|
662
|
+
const ZOPKIT_PROMPTS_CATALOG = ['ship-feature', 'codebase-recon', 'security-sweep', 'merge-guide', 'review-pr']
|
|
663
|
+
|
|
664
|
+
// Recommended kit for detected category
|
|
665
|
+
const KIT_RECOMMENDATIONS = {
|
|
666
|
+
builder: { skills: ['zop-blueprint', 'session-start'], agents: ['zop-shipper'], prompts: ['ship-feature'] },
|
|
667
|
+
scout: { skills: ['zop-recon', 'session-start'], agents: ['zop-scout'], prompts: ['codebase-recon'] },
|
|
668
|
+
security: { skills: ['zop-guard-skill', 'session-start'], agents: ['zop-guard'], prompts: ['security-sweep'] },
|
|
669
|
+
merge: { skills: ['zop-merge-skill', 'session-start'], agents: ['zop-merger'], prompts: ['merge-guide'] },
|
|
670
|
+
review: { skills: ['zop-review-skill', 'session-start'], agents: ['zop-reviewer'], prompts: ['review-pr'] },
|
|
671
|
+
coordinator: { skills: ['zop-blueprint', 'zop-recon', 'session-start'], agents: ['zop-orchestrator'], prompts: ['ship-feature'] },
|
|
672
|
+
}
|
|
673
|
+
const recommendedKit = KIT_RECOMMENDATIONS[detectedKitCategory] || KIT_RECOMMENDATIONS.builder
|
|
674
|
+
|
|
675
|
+
// What's already in the project vs what's missing
|
|
676
|
+
const missingSkills = recommendedKit.skills.filter(s => !existingAgentConfig.skills.includes(s))
|
|
677
|
+
const missingAgents = recommendedKit.agents.filter(a => !existingAgentConfig.subagents.includes(a))
|
|
678
|
+
const missingPrompts = recommendedKit.prompts.filter(p => !existingAgentConfig.prompts.includes(p))
|
|
679
|
+
|
|
680
|
+
// Task-level overrides: enable recommended, disable irrelevant
|
|
681
|
+
const taskKitOverrides = {
|
|
682
|
+
skills: Object.fromEntries([
|
|
683
|
+
...existingAgentConfig.skills.map(s => [s, recommendedKit.skills.includes(s)]),
|
|
684
|
+
...recommendedKit.skills.filter(s => !existingAgentConfig.skills.includes(s)).map(s => [s, false]), // not in project yet — pending suggest_skill
|
|
685
|
+
]),
|
|
686
|
+
subagents: Object.fromEntries(existingAgentConfig.subagents.map(a => [a, recommendedKit.agents.includes(a)])),
|
|
687
|
+
prompts: Object.fromEntries(existingAgentConfig.prompts.map(p => [p, recommendedKit.prompts.includes(p)])),
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Stack-derived codebase facts that should be persisted via remember()
|
|
691
|
+
const rememberFacts = [
|
|
692
|
+
stack.language && { key: 'stack_language', value: stack.language },
|
|
693
|
+
stack.framework && { key: 'stack_framework', value: stack.framework },
|
|
694
|
+
stack.testRunner && { key: 'stack_test_runner', value: stack.testRunner },
|
|
695
|
+
stack.packageManager && { key: 'stack_pkg_manager', value: stack.packageManager },
|
|
696
|
+
entryPoints.routes?.[0] && { key: 'entry_routes', value: entryPoints.routes[0] },
|
|
697
|
+
entryPoints.models?.[0] && { key: 'entry_models', value: entryPoints.models[0] },
|
|
698
|
+
entryPoints.components?.[0] && { key: 'entry_components', value: entryPoints.components[0] },
|
|
699
|
+
].filter(Boolean)
|
|
700
|
+
|
|
701
|
+
// Skill suggestion templates for each missing item
|
|
702
|
+
const SKILL_BODY_TEMPLATES = {
|
|
703
|
+
'zop-blueprint': `## Feature Blueprint\n1. Define the data model / schema changes\n2. Build the API layer (route → service → model)\n3. Build the frontend (component → hook → API call)\n4. Wire everything together\n5. Write tests (unit + integration)\n6. Update README / docs`,
|
|
704
|
+
'zop-recon': `## Codebase Recon\n1. Read entry points: routes, models, middleware, utils\n2. Map the data flow end-to-end\n3. Identify patterns: naming conventions, error handling, auth\n4. Find gaps: missing tests, dead code, undocumented APIs\n5. Write findings to scout_task`,
|
|
705
|
+
'zop-guard-skill': `## Security Audit\n1. Check authentication (JWT expiry, refresh, revocation)\n2. Check authorization (role checks on every sensitive route)\n3. Check input validation (Zod/Joi on all user inputs)\n4. Check for injection (SQL, NoSQL, command)\n5. Check error messages (no stack traces exposed)\n6. Run OWASP Top 10 checklist`,
|
|
706
|
+
'zop-merge-skill': `## Merge & Ship\n1. git fetch origin && git rebase origin/main\n2. Resolve any conflicts using resolve_conflict tool\n3. Run full test suite — must be green\n4. Push with --force-with-lease\n5. Verify PR CI passes\n6. Request final review`,
|
|
707
|
+
'zop-review-skill': `## Code Review\n1. Read the task README to understand intent\n2. Review each changed file: correctness, edge cases, security\n3. Check test coverage: are new paths tested?\n4. Check for breaking changes\n5. Post review with specific file+line comments\n6. Approve or request changes`,
|
|
708
|
+
'session-start': `## Session Start\n1. Call recall(taskId) to restore memory from previous sessions\n2. Call get_agent_context(taskId) for the full implementation plan\n3. Check local git state: \`git status\`\n4. Check branch: \`git branch --show-current\`\n5. Read any changed files since last session\n6. Resume work from where you left off`,
|
|
709
|
+
}
|
|
710
|
+
const AGENT_BODY_TEMPLATES = {
|
|
711
|
+
'zop-shipper': `You are a builder agent. Your job is to implement code end-to-end.\nConstraints:\n- Only modify files listed in claimedFiles\n- Every change needs a test\n- Run tests before marking done\n- Never modify auth middleware without explicit approval`,
|
|
712
|
+
'zop-scout': `You are a scout agent. Your job is to map the codebase — READ ONLY.\nConstraints:\n- Never write or modify any file\n- Read every source file systematically\n- Write findings via scout_task tool\n- Focus on: data flow, auth, API surface, test coverage, gaps`,
|
|
713
|
+
'zop-guard': `You are a security audit agent.\nConstraints:\n- Check auth, authorization, input validation, and OWASP Top 10\n- Report findings as structured issues, not inline comments\n- Never modify security-critical code without explicit admin approval`,
|
|
714
|
+
'zop-orchestrator': `You are a coordinator agent. Your job is to decompose and delegate — NO CODE.\nConstraints:\n- Call decompose_task to break work into parallel subtasks\n- Call get_parallel_kickoffs to generate builder prompts\n- Never implement anything yourself\n- Each subtask must have exclusive file ownership`,
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
const suggestForProject = [
|
|
718
|
+
...missingSkills.map(name => ({
|
|
719
|
+
type: 'skill',
|
|
720
|
+
name,
|
|
721
|
+
description: `${name} — ${ZOPKIT_SKILLS_CATALOG.includes(name) ? 'ZopKit standard skill' : 'custom skill'}`,
|
|
722
|
+
body: SKILL_BODY_TEMPLATES[name] || `## ${name}\n[Add instructions here]`,
|
|
723
|
+
rationale: `Detected task type: ${detectedKitCategory}. This skill guides the builder through the ${name.replace('zop-', '').replace('-skill', '')} process in this codebase.`,
|
|
724
|
+
})),
|
|
725
|
+
...missingAgents.map(name => ({
|
|
726
|
+
type: 'subagent',
|
|
727
|
+
name,
|
|
728
|
+
description: `${name} — ${ZOPKIT_AGENTS_CATALOG.includes(name) ? 'ZopKit standard agent' : 'custom agent'}`,
|
|
729
|
+
body: AGENT_BODY_TEMPLATES[name] || `## ${name} Agent\n[Add role instructions here]`,
|
|
730
|
+
rationale: `Detected task type: ${detectedKitCategory}. This agent persona enforces the right constraints for ${name.replace('zop-', '')} sessions.`,
|
|
731
|
+
})),
|
|
732
|
+
]
|
|
733
|
+
|
|
734
|
+
// ── 5. Return codebase intelligence + tight instructions ──────────────────
|
|
636
735
|
const hasEntryPoints = Object.keys(entryPoints).length > 0
|
|
637
736
|
const stackSummary = [
|
|
638
737
|
stack.language, stack.framework, stack.testRunner, ...(stack.extra || [])
|
|
@@ -649,6 +748,26 @@ Do NOT ask the developer to describe the codebase — read it yourself.`,
|
|
|
649
748
|
: { note: 'No entry points found at cwd — check that MCP server is running from the project root' },
|
|
650
749
|
},
|
|
651
750
|
|
|
751
|
+
// Agent config analysis — act on this BEFORE calling create_task
|
|
752
|
+
agentConfigAnalysis: {
|
|
753
|
+
detectedKitCategory,
|
|
754
|
+
recommendedKit,
|
|
755
|
+
existingProjectConfig: existingAgentConfig,
|
|
756
|
+
missingFromProject: { skills: missingSkills, agents: missingAgents, prompts: missingPrompts },
|
|
757
|
+
suggestForProject, // call suggest_skill for each item here
|
|
758
|
+
taskKitOverrides, // pass as agentKitOverrides to create_task
|
|
759
|
+
rememberFacts, // call remember(taskId, key, value) for each after create_task
|
|
760
|
+
instruction: [
|
|
761
|
+
suggestForProject.length > 0
|
|
762
|
+
? `⚠️ ${suggestForProject.length} item(s) are missing from project config — call suggest_skill for each before creating the task.`
|
|
763
|
+
: `✅ Project config already has the recommended kit for ${detectedKitCategory} tasks.`,
|
|
764
|
+
`Pass taskKitOverrides as agentKitOverrides in create_task to pre-configure this task.`,
|
|
765
|
+
rememberFacts.length > 0
|
|
766
|
+
? `After create_task, call remember(taskId, key, value) for ${rememberFacts.length} codebase fact(s) so future sessions don't need to re-scout.`
|
|
767
|
+
: null,
|
|
768
|
+
].filter(Boolean).join('\n'),
|
|
769
|
+
},
|
|
770
|
+
|
|
652
771
|
// Duplicate guard
|
|
653
772
|
...(similarTasks?.length > 0 && {
|
|
654
773
|
duplicateWarning: true,
|
|
@@ -656,16 +775,21 @@ Do NOT ask the developer to describe the codebase — read it yourself.`,
|
|
|
656
775
|
duplicateInstruction: 'Check the list above. If one matches → call kickoff_task on it. If none match → proceed.',
|
|
657
776
|
}),
|
|
658
777
|
|
|
659
|
-
// What to do now
|
|
778
|
+
// What to do now
|
|
660
779
|
nextSteps: [
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
'
|
|
666
|
-
|
|
780
|
+
suggestForProject.length > 0
|
|
781
|
+
? `FIRST: call suggest_skill for each item in agentConfigAnalysis.suggestForProject (${suggestForProject.length} item(s)). Use the body and rationale from the array.`
|
|
782
|
+
: null,
|
|
783
|
+
'Read 2-3 files from entryPoints.routes / entryPoints.models / entryPoints.components to extract naming conventions.',
|
|
784
|
+
'Draft readmeMarkdown: ## Goal, ## Stack, ## Technical approach, ## Files to create, ## Files to modify, ## Subtasks, ## Acceptance criteria.',
|
|
785
|
+
`Call create_task(projectId="${projectId}", title="<verb+noun>", readmeMarkdown="<plan>", taskType="<type>", priority="${priority}", column="todo", subtasks=[...], suggestedFiles=[...], agentKitOverrides=agentConfigAnalysis.taskKitOverrides)`,
|
|
786
|
+
'After create_task: call remember(taskId, key, value) for each fact in agentConfigAnalysis.rememberFacts.',
|
|
787
|
+
'Then: call log_session_event(taskId, type="info", name="task-created", summary="Codebase scouted, kit configured, task created").',
|
|
788
|
+
'Then: call kickoff_task(taskId, confirmed=false) to preview the plan and check git state.',
|
|
789
|
+
'Then: call submit_task_for_approval(taskId, summary="<3-8 sentence summary>", reviewerId="<admin id>").',
|
|
790
|
+
'After approval: call kickoff_task(taskId, confirmed=true, agentRole="builder", files=[...suggestedFiles]).',
|
|
791
|
+
].filter(Boolean),
|
|
667
792
|
|
|
668
|
-
// Context
|
|
669
793
|
request,
|
|
670
794
|
projectId,
|
|
671
795
|
priority,
|
|
@@ -715,8 +839,13 @@ Always prefer column="todo" so the task is visibly ready to start.`,
|
|
|
715
839
|
.describe('Ordered implementation checklist — shown in the task UI and read by agents at kickoff. Order matters: schema → model → service → route → test → frontend.'),
|
|
716
840
|
suggestedFiles: z.array(z.string()).optional()
|
|
717
841
|
.describe('Files the builder will claim at kickoff (e.g. ["server/routes/tasks.js", "server/models/Task.js"]). Included in the task README automatically so the builder knows what to pass to kickoff_task files=[...].'),
|
|
842
|
+
agentKitOverrides: z.object({
|
|
843
|
+
skills: z.record(z.boolean()).optional(),
|
|
844
|
+
subagents: z.record(z.boolean()).optional(),
|
|
845
|
+
prompts: z.record(z.boolean()).optional(),
|
|
846
|
+
}).optional().describe('Task-level skill/agent/prompt overrides — pass agentConfigAnalysis.taskKitOverrides from plan_task_from_codebase to pre-configure the right kit for this task.'),
|
|
718
847
|
},
|
|
719
|
-
async ({ projectId, suggestedFiles, ...taskData }) => {
|
|
848
|
+
async ({ projectId, suggestedFiles, agentKitOverrides, ...taskData }) => {
|
|
720
849
|
try { assertProjectScope(projectId) } catch (e) { return errorText(e.message) }
|
|
721
850
|
|
|
722
851
|
// Append suggested files section to the README so the builder sees them at kickoff
|
|
@@ -736,18 +865,37 @@ Always prefer column="todo" so the task is visibly ready to start.`,
|
|
|
736
865
|
if (!res?.success) return errorText(res?.message || 'Failed to create task')
|
|
737
866
|
|
|
738
867
|
const task = res.data?.task
|
|
868
|
+
|
|
869
|
+
// Apply agentKitOverrides immediately if provided
|
|
870
|
+
let kitApplied = null
|
|
871
|
+
if (task?._id && agentKitOverrides && Object.keys(agentKitOverrides).length > 0) {
|
|
872
|
+
try {
|
|
873
|
+
const kitRes = await api.patch(`/api/tasks/${task._id}`, { agentKitOverrides })
|
|
874
|
+
kitApplied = kitRes?.success
|
|
875
|
+
? { applied: true, overrides: agentKitOverrides }
|
|
876
|
+
: { applied: false, reason: kitRes?.message }
|
|
877
|
+
} catch { kitApplied = { applied: false, reason: 'patch failed' } }
|
|
878
|
+
}
|
|
879
|
+
|
|
739
880
|
return text({
|
|
740
|
-
created:
|
|
741
|
-
taskId:
|
|
742
|
-
taskKey:
|
|
743
|
-
title:
|
|
744
|
-
column:
|
|
745
|
-
taskType:
|
|
746
|
-
subtasks:
|
|
747
|
-
|
|
748
|
-
|
|
881
|
+
created: true,
|
|
882
|
+
taskId: task?._id,
|
|
883
|
+
taskKey: task?.key,
|
|
884
|
+
title: task?.title,
|
|
885
|
+
column: task?.column,
|
|
886
|
+
taskType: task?.taskType || null,
|
|
887
|
+
subtasks: (task?.subtasks || []).length,
|
|
888
|
+
kitApplied,
|
|
889
|
+
nextStep: task?._id
|
|
890
|
+
? [
|
|
891
|
+
`1. Call remember(taskId="${task._id}", key, value) for each fact in agentConfigAnalysis.rememberFacts.`,
|
|
892
|
+
`2. Call log_session_event(taskId="${task._id}", type="info", name="task-created", summary="Codebase scouted, kit configured").`,
|
|
893
|
+
`3. Call kickoff_task(taskId="${task._id}", confirmed=false) — read the plan and check local git state.`,
|
|
894
|
+
`4. Call submit_task_for_approval(taskId="${task._id}", summary="<3-8 sentences>", reviewerId="<admin id>").`,
|
|
895
|
+
`5. After approval: call kickoff_task(taskId="${task._id}", confirmed=true, agentRole="builder", files=[...]).`,
|
|
896
|
+
].join('\n')
|
|
749
897
|
: null,
|
|
750
|
-
message: `Task ${task?.key} created
|
|
898
|
+
message: `Task ${task?.key} created${kitApplied?.applied ? ' with kit overrides applied' : ''}. Follow nextStep — do NOT branch until the plan is approved.`,
|
|
751
899
|
})
|
|
752
900
|
}
|
|
753
901
|
)
|
|
@@ -815,7 +963,7 @@ WHAT THIS DOES AUTOMATICALLY (confirmed=true):
|
|
|
815
963
|
1. Verifies all changes are committed and pushed — blocks if not
|
|
816
964
|
2. Auto-pushes any unpushed commits to remote
|
|
817
965
|
3. Saves your park note (summary, remaining, blockers) to the task
|
|
818
|
-
4. Deletes
|
|
966
|
+
4. Deletes all workspace files (.cursor/rules/, .cursor/skills/, .cursor/agents/) so they don't bleed into other tasks
|
|
819
967
|
5. Posts a comment on the task with your handoff notes (visible to Dev B)
|
|
820
968
|
6. Notifies all project members that this task is parked and ready for pickup
|
|
821
969
|
|
|
@@ -875,8 +1023,10 @@ Set confirmed=false first to preview, then confirmed=true to execute everything.
|
|
|
875
1023
|
// ── Auto-commit if there are uncommitted changes ───────────────────────
|
|
876
1024
|
if (repoRoot && uncommitted) {
|
|
877
1025
|
try {
|
|
878
|
-
const
|
|
879
|
-
|
|
1026
|
+
const rawMsg = `wip(${task?.key?.toLowerCase() || 'task'}): ${(summary || 'parking task').slice(0, 60)}`
|
|
1027
|
+
// Sanitize to prevent shell injection — strip/replace special chars
|
|
1028
|
+
const commitMsg = rawMsg.replace(/"/g, "'").replace(/[`$\\]/g, '')
|
|
1029
|
+
runGit('add -u', repoRoot) // only tracked files — avoids staging .env / build artifacts
|
|
880
1030
|
runGit(`commit -m "${commitMsg}"`, repoRoot)
|
|
881
1031
|
unpushedCount += 1 // just committed, so now there's something to push
|
|
882
1032
|
} catch (e) {
|
|
@@ -939,10 +1089,12 @@ WHAT THIS DOES AUTOMATICALLY (confirmed=true):
|
|
|
939
1089
|
2. Shows the last 5 commits on the branch so you know the exact state of the code
|
|
940
1090
|
3. Shows recent task comments so you have full context
|
|
941
1091
|
4. Runs: git fetch origin + git checkout <branch> + git pull (switches your local repo)
|
|
942
|
-
5. Restores the cursor
|
|
943
|
-
6.
|
|
944
|
-
7.
|
|
1092
|
+
5. Restores the FULL cursor workspace: rules, skills, session-protocol, active-agent.md
|
|
1093
|
+
6. Logs all restored skills/rules to the session activity timeline
|
|
1094
|
+
7. Posts a comment that you picked up the task
|
|
1095
|
+
8. Notifies the previous developer that you have taken over
|
|
945
1096
|
|
|
1097
|
+
After unparking: READ all injected skills before touching any files. Then re-claim files.
|
|
946
1098
|
Set confirmed=false first to read everything, then confirmed=true to execute.`,
|
|
947
1099
|
{
|
|
948
1100
|
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
@@ -992,6 +1144,8 @@ Set confirmed=false first to read everything, then confirmed=true to execute.`,
|
|
|
992
1144
|
`git checkout ${branch}`,
|
|
993
1145
|
`git pull origin ${branch}`,
|
|
994
1146
|
task?.cursorRules?.trim() ? `Restore .cursor/rules/${task.key?.toLowerCase()}.mdc` : null,
|
|
1147
|
+
`Restore full cursor workspace (skills, session-protocol, active-agent.md)`,
|
|
1148
|
+
`Log restored workspace files to session timeline`,
|
|
995
1149
|
'Post "picked up" comment on task',
|
|
996
1150
|
'Notify previous developer',
|
|
997
1151
|
].filter(Boolean) : ['No branch linked — create one with create_branch after unparking'],
|
|
@@ -1081,6 +1235,37 @@ Set confirmed=false first to read everything, then confirmed=true to execute.`,
|
|
|
1081
1235
|
api.patch(`/api/tasks/${taskId}`, { agentRole }).catch(() => {/* non-fatal */})
|
|
1082
1236
|
}
|
|
1083
1237
|
|
|
1238
|
+
// ── Re-write full cursor workspace (skills, session protocol, active-agent) ──
|
|
1239
|
+
// park_task deletes everything; unpark_task must restore the complete workspace.
|
|
1240
|
+
let workspaceResult = null
|
|
1241
|
+
try {
|
|
1242
|
+
let projectAgentConfig = null
|
|
1243
|
+
try {
|
|
1244
|
+
const projRes = await api.get(`/api/projects/${task.project}`)
|
|
1245
|
+
if (projRes?.success) projectAgentConfig = projRes.data.project?.agentConfig || null
|
|
1246
|
+
} catch { /* non-fatal */ }
|
|
1247
|
+
|
|
1248
|
+
const taskForWorkspace = agentRole ? { ...task, agentRole } : task
|
|
1249
|
+
workspaceResult = writeCursorWorkspace(taskForWorkspace, projectAgentConfig, repoPath || process.cwd())
|
|
1250
|
+
|
|
1251
|
+
// Log injected skills/rules to session timeline
|
|
1252
|
+
// Log each written file to session timeline
|
|
1253
|
+
const writtenFiles = workspaceResult?.written || []
|
|
1254
|
+
for (const filePath of writtenFiles) {
|
|
1255
|
+
const fileName = filePath.split('/').pop().replace(/\.(md|mdc)$/, '')
|
|
1256
|
+
const isSkill = filePath.includes('/.cursor/skills/')
|
|
1257
|
+
const isRule = filePath.includes('/.cursor/rules/')
|
|
1258
|
+
const isAgent = filePath.includes('/.cursor/agents/')
|
|
1259
|
+
const type = isSkill ? 'skill' : isRule ? 'rule' : isAgent ? 'subagent' : 'info'
|
|
1260
|
+
api.post(`/api/tasks/${taskId}/session/log`, {
|
|
1261
|
+
type,
|
|
1262
|
+
name: fileName,
|
|
1263
|
+
role: agentRole || null,
|
|
1264
|
+
summary: `📖 Injected at unpark: ${filePath.replace(workspaceResult.repoRoot, '')}`,
|
|
1265
|
+
}).catch(() => {})
|
|
1266
|
+
}
|
|
1267
|
+
} catch { /* non-fatal — workspace restore failure should not block the unpark */ }
|
|
1268
|
+
|
|
1084
1269
|
// ── #9 Last commit metadata for handoff context ───────────────────────
|
|
1085
1270
|
const lastCommit = getLastCommitMeta(repoRoot)
|
|
1086
1271
|
|
|
@@ -1093,21 +1278,30 @@ Set confirmed=false first to read everything, then confirmed=true to execute.`,
|
|
|
1093
1278
|
cursorRules: cursorRulesFile
|
|
1094
1279
|
? { restored: true, path: cursorRulesFile, agentRole: agentRole || null }
|
|
1095
1280
|
: { restored: false },
|
|
1281
|
+
workspace: workspaceResult
|
|
1282
|
+
? { restored: true, filesCount: workspaceResult.written?.length || 0, files: workspaceResult.written || [] }
|
|
1283
|
+
: { restored: false },
|
|
1096
1284
|
commentPosted: true,
|
|
1097
1285
|
previousDevNotified: true,
|
|
1098
1286
|
parkNote: task?.parkNote || null,
|
|
1099
1287
|
message: gitResult?.switched
|
|
1100
|
-
? `You are now on branch "${branch}". Cursor
|
|
1101
|
-
: `Branch switch failed — see git.manualSteps. Cursor
|
|
1288
|
+
? `You are now on branch "${branch}". Cursor workspace restored${agentRole ? ` (role: ${agentRole})` : ''}. Read all skills before editing.`
|
|
1289
|
+
: `Branch switch failed — see git.manualSteps. Cursor workspace restored.`,
|
|
1290
|
+
previouslyClaimed: task?.claimedFiles?.length ? task.claimedFiles : null,
|
|
1102
1291
|
nextStep: agentRole === 'builder'
|
|
1103
|
-
?
|
|
1292
|
+
? [
|
|
1293
|
+
`BUILDER role active.`,
|
|
1294
|
+
`1. ⚠️ READ all injected skills first (listed in workspace.files) — mandatory before any edits`,
|
|
1295
|
+
`2. Re-claim your files: claim_files(taskId="${taskId}", files=${JSON.stringify(task?.claimedFiles || [])})`,
|
|
1296
|
+
`3. Then continue implementation from where the previous developer left off`,
|
|
1297
|
+
].join('\n')
|
|
1104
1298
|
: agentRole === 'scout'
|
|
1105
1299
|
? `SCOUT role active. Read-only mode — map the codebase and save findings with update_task(scoutReport=...).`
|
|
1106
1300
|
: agentRole === 'coordinator'
|
|
1107
1301
|
? `COORDINATOR role active. Call decompose_task to plan parallel workstreams.`
|
|
1108
1302
|
: agentRole === 'reviewer'
|
|
1109
1303
|
? `REVIEWER role active. Call review_pr to start the review chain.`
|
|
1110
|
-
:
|
|
1304
|
+
: `Read all injected skills in workspace.files before making any changes.`,
|
|
1111
1305
|
})
|
|
1112
1306
|
}
|
|
1113
1307
|
)
|
|
@@ -1389,52 +1583,92 @@ Call confirmed=false to preview the decomposition, confirmed=true to save it.`,
|
|
|
1389
1583
|
templates: decompositionTemplates,
|
|
1390
1584
|
} : null,
|
|
1391
1585
|
warnings: [
|
|
1392
|
-
!executionPlan.scoutReportPresent
|
|
1393
|
-
|
|
1586
|
+
!executionPlan.scoutReportPresent
|
|
1587
|
+
? '⛔ SCOUT REPORT MISSING — Strongly recommended before decomposing. Builders without a scout map will guess at file ownership and produce overlapping work. Call kickoff_task(agentRole="scout", confirmed=true) first.'
|
|
1588
|
+
: null,
|
|
1589
|
+
!executionPlan.readmePresent
|
|
1590
|
+
? '⛔ NO IMPLEMENTATION PLAN — Builders cannot implement without a spec. Call update_task(readmeMarkdown=...) before decomposing.'
|
|
1591
|
+
: null,
|
|
1592
|
+
subtaskPlan.some(s => !s.files?.length)
|
|
1593
|
+
? `⚠️ ${subtaskPlan.filter(s => !s.files?.length).map(s => s.title).join(', ')} — no files declared. Every subtask MUST own at least one file to prevent two builders editing the same code.`
|
|
1594
|
+
: null,
|
|
1394
1595
|
].filter(Boolean),
|
|
1596
|
+
scoutFirstInstruction: !executionPlan.scoutReportPresent
|
|
1597
|
+
? `MANDATORY: call scout_task(taskId="${task._id}", confirmed=false) then scout_task(confirmed=true, report="<your findings>") before confirming this decomposition. Do NOT switch agentRole — run scout_task directly as coordinator.`
|
|
1598
|
+
: null,
|
|
1395
1599
|
requiresConfirmation: true,
|
|
1396
1600
|
message: 'Review the decomposition above. Call decompose_task again with confirmed=true to save it and create the subtasks on the board.',
|
|
1397
1601
|
})
|
|
1398
1602
|
}
|
|
1399
1603
|
|
|
1400
|
-
// Save decomposition JSON to parent task
|
|
1401
|
-
const decompositionJson = JSON.stringify(executionPlan, null, 2)
|
|
1402
|
-
try {
|
|
1403
|
-
await api.patch(`/api/tasks/${taskId}`, { decomposition: decompositionJson })
|
|
1404
|
-
} catch { /* non-fatal */ }
|
|
1405
|
-
|
|
1406
1604
|
// Create real child tasks for each subtask in the plan
|
|
1605
|
+
// Do this BEFORE saving the decomposition JSON so we can embed child task IDs.
|
|
1407
1606
|
const projectId = task.project?._id || task.project
|
|
1408
1607
|
const createdTasks = []
|
|
1608
|
+
// Build a map from subtask title → child task info for embedding into executionPlan
|
|
1609
|
+
const childTaskByTitle = {}
|
|
1610
|
+
|
|
1409
1611
|
for (const s of subtaskPlan) {
|
|
1410
1612
|
try {
|
|
1613
|
+
// Role → generic role-appropriate skills that any project should have.
|
|
1614
|
+
// We avoid hardcoding third-party skill names (e.g. zop-*) because projects
|
|
1615
|
+
// may not have those configured — the agent would look for files that don't exist.
|
|
1616
|
+
// The project-level agentConfig.skills drive what actually gets written to .cursor/skills/.
|
|
1617
|
+
const ROLE_KIT = {
|
|
1618
|
+
builder: { skills: { 'run-tests': true, 'commit-conventions': true }, subagents: {}, prompts: {} },
|
|
1619
|
+
scout: { skills: { 'scout-codebase': true }, subagents: {}, prompts: {} },
|
|
1620
|
+
reviewer: { skills: { 'review-checklist': true, 'run-tests': true }, subagents: {}, prompts: {} },
|
|
1621
|
+
coordinator: { skills: { 'scout-codebase': true, 'run-tests': true }, subagents: {}, prompts: {} },
|
|
1622
|
+
}
|
|
1623
|
+
const childKit = ROLE_KIT[s.role] || ROLE_KIT.builder
|
|
1624
|
+
|
|
1411
1625
|
const childRes = await api.post(`/api/projects/${projectId}/tasks`, {
|
|
1412
1626
|
title: `[${s.role.toUpperCase()}] ${s.title}`,
|
|
1413
1627
|
description: s.description,
|
|
1414
1628
|
readmeMarkdown: [
|
|
1415
1629
|
`## Role: ${s.role}`,
|
|
1630
|
+
`## Parent task: ${task.key} — ${task.title}`,
|
|
1416
1631
|
`## Description\n${s.description}`,
|
|
1417
1632
|
s.files?.length ? `## Files to claim at kickoff\n${s.files.map(f => `- \`${f}\``).join('\n')}` : '',
|
|
1418
|
-
s.dependsOn?.length ? `## Depends on\n${s.dependsOn.map(d => `- ${d}`).join('\n')}` : '',
|
|
1633
|
+
s.dependsOn?.length ? `## Depends on (must complete before this starts)\n${s.dependsOn.map(d => `- ${d}`).join('\n')}` : '',
|
|
1634
|
+
`## Skills to read BEFORE editing\nRead every .md file in .cursor/skills/ that was written at kickoff.`,
|
|
1635
|
+
`## After finishing\nCall park_task or raise_pr, then update the parent task (${task.key}) subtask checklist.`,
|
|
1419
1636
|
].filter(Boolean).join('\n\n'),
|
|
1420
|
-
column:
|
|
1421
|
-
priority:
|
|
1422
|
-
taskType:
|
|
1423
|
-
parentTask:
|
|
1637
|
+
column: 'todo',
|
|
1638
|
+
priority: task.priority || 'medium',
|
|
1639
|
+
taskType: s.role === 'reviewer' ? 'feature' : (task.taskType || 'feature'),
|
|
1640
|
+
parentTask: taskId,
|
|
1424
1641
|
suggestedFiles: s.files || [],
|
|
1642
|
+
agentKitOverrides: childKit,
|
|
1425
1643
|
})
|
|
1426
1644
|
if (childRes?.success) {
|
|
1427
|
-
|
|
1645
|
+
const childInfo = {
|
|
1428
1646
|
taskId: childRes.data?.task?._id,
|
|
1429
1647
|
taskKey: childRes.data?.task?.key,
|
|
1430
1648
|
title: childRes.data?.task?.title,
|
|
1431
1649
|
role: s.role,
|
|
1432
1650
|
files: s.files,
|
|
1433
|
-
}
|
|
1651
|
+
}
|
|
1652
|
+
createdTasks.push(childInfo)
|
|
1653
|
+
childTaskByTitle[s.title] = childInfo
|
|
1434
1654
|
}
|
|
1435
1655
|
} catch { /* non-fatal — continue creating remaining tasks */ }
|
|
1436
1656
|
}
|
|
1437
1657
|
|
|
1658
|
+
// Embed child task IDs into the execution plan so get_parallel_kickoffs can route
|
|
1659
|
+
// each builder to their own child task (not the parent task).
|
|
1660
|
+
executionPlan.subtasks = executionPlan.subtasks.map(s => ({
|
|
1661
|
+
...s,
|
|
1662
|
+
childTaskId: childTaskByTitle[s.title]?.taskId || null,
|
|
1663
|
+
childTaskKey: childTaskByTitle[s.title]?.taskKey || null,
|
|
1664
|
+
}))
|
|
1665
|
+
|
|
1666
|
+
// Save decomposition JSON to parent task — now includes child task IDs
|
|
1667
|
+
const decompositionJson = JSON.stringify(executionPlan, null, 2)
|
|
1668
|
+
try {
|
|
1669
|
+
await api.patch(`/api/tasks/${taskId}`, { decomposition: decompositionJson })
|
|
1670
|
+
} catch { /* non-fatal */ }
|
|
1671
|
+
|
|
1438
1672
|
return text({
|
|
1439
1673
|
decomposed: true,
|
|
1440
1674
|
taskKey: task.key,
|
|
@@ -1455,16 +1689,17 @@ Call confirmed=false to preview the decomposition, confirmed=true to save it.`,
|
|
|
1455
1689
|
`Scout agent entry point: analyze the codebase for a task and save a structured scout report.
|
|
1456
1690
|
|
|
1457
1691
|
Call this BEFORE builders start when the Coordinator needs a codebase map.
|
|
1458
|
-
|
|
1692
|
+
Coordinators call this directly — do NOT switch agentRole to "scout" to call it.
|
|
1693
|
+
Agents with agentRole="scout" also call this as their primary work tool.
|
|
1459
1694
|
|
|
1460
1695
|
Two-phase flow:
|
|
1461
1696
|
Phase 1 — confirmed=false: get the briefing (what to analyze, report format)
|
|
1462
1697
|
Phase 2 — confirmed=true + report: save your findings to the task
|
|
1463
1698
|
|
|
1464
1699
|
The scout report is consumed by:
|
|
1465
|
-
- decompose_task (warns if missing)
|
|
1466
|
-
- get_agent_context (included in builder
|
|
1467
|
-
- kickoff_task (surfaced as part of builder brief)
|
|
1700
|
+
- decompose_task (warns if missing, refuses to decompose without one)
|
|
1701
|
+
- get_agent_context (automatically included in builder system prompt)
|
|
1702
|
+
- kickoff_task (surfaced as part of the builder brief)
|
|
1468
1703
|
|
|
1469
1704
|
Scouts MUST NOT modify any source code files or create branches.`,
|
|
1470
1705
|
{
|
|
@@ -1781,11 +2016,24 @@ This tool posts a comment on the task (for visibility in the app) and notifies a
|
|
|
1781
2016
|
trackTaskActivity(taskId, 'request_human_input')
|
|
1782
2017
|
await api.post(`/api/tasks/${taskId}/ask-human`, { question, context, type }).catch(() => null)
|
|
1783
2018
|
|
|
2019
|
+
// Detect whether we're likely running as a Cursor Background Agent (non-interactive).
|
|
2020
|
+
// Background agents cannot receive chat replies — the human must check the InternalTool
|
|
2021
|
+
// task comments to see the question, then the agent should use a safe default and move on.
|
|
2022
|
+
// We surface this explicitly so the agent does not pause indefinitely.
|
|
2023
|
+
const isBackgroundLikely = !process.stdin.isTTY
|
|
2024
|
+
|
|
1784
2025
|
const typeLabel = type === 'approval' ? '🔐 APPROVAL NEEDED' : type === 'ambiguity' ? '🤔 AMBIGUITY — YOUR DECISION' : '❓ QUESTION FOR YOU'
|
|
1785
2026
|
return text({
|
|
1786
2027
|
[typeLabel]: question,
|
|
1787
2028
|
...(context ? { context } : {}),
|
|
1788
|
-
|
|
2029
|
+
backgroundAgentWarning: isBackgroundLikely
|
|
2030
|
+
? 'You appear to be running as a Background Agent (no interactive TTY). The question has been posted as a task comment and the developer has been notified. Since there is no interactive chat channel, DO NOT wait indefinitely. Use the safest autonomous default and proceed, then note your decision in a log_session_event call.'
|
|
2031
|
+
: null,
|
|
2032
|
+
instruction: isBackgroundLikely
|
|
2033
|
+
? 'Background agent mode: proceed with the safest default decision. Log your choice with log_session_event(type="info", name="autonomous-decision", summary="<what you decided and why>").'
|
|
2034
|
+
: 'Please reply in this Cursor chat. I will use your answer to continue. Do NOT proceed until you reply.',
|
|
2035
|
+
taskCommentPosted: true,
|
|
2036
|
+
developerNotified: true,
|
|
1789
2037
|
})
|
|
1790
2038
|
}
|
|
1791
2039
|
)
|
|
@@ -1912,16 +2160,58 @@ Returns systemPrompt ready to use as a Claude system prompt.`,
|
|
|
1912
2160
|
} catch { /* non-fatal */ }
|
|
1913
2161
|
}
|
|
1914
2162
|
|
|
1915
|
-
//
|
|
1916
|
-
//
|
|
2163
|
+
// Filter skills to only those relevant to this role and task type.
|
|
2164
|
+
// An agent should NOT load every project skill — only what it needs for this specific task.
|
|
1917
2165
|
const allProjectSkills = ctx.project?.agentConfig?.skills || []
|
|
1918
2166
|
const taskSkillOverrides = ctx.task?.agentKitOverrides?.skills || {}
|
|
1919
|
-
const
|
|
2167
|
+
const enabledSkills = allProjectSkills.filter(s => {
|
|
1920
2168
|
const taskOverride = taskSkillOverrides[s.name]
|
|
1921
2169
|
return taskOverride !== undefined ? taskOverride : s.enabled !== false
|
|
1922
2170
|
})
|
|
1923
|
-
|
|
1924
|
-
|
|
2171
|
+
|
|
2172
|
+
// Role-based relevance filter: each role only gets skills that match its job.
|
|
2173
|
+
// Keywords for each role are matched against skill name + description.
|
|
2174
|
+
const ROLE_SKILL_KEYWORDS = {
|
|
2175
|
+
builder: ['test', 'run', 'build', 'lint', 'commit', 'style', 'deploy', 'migration', 'seed',
|
|
2176
|
+
'implement', 'feature', 'api', 'endpoint', 'component', 'service', 'hook',
|
|
2177
|
+
'util', 'helper', 'create', 'update', 'delete', 'fix', 'patch', 'convention'],
|
|
2178
|
+
scout: ['scout', 'codebase', 'explore', 'map', 'analyze', 'read', 'report',
|
|
2179
|
+
'find', 'locate', 'search', 'survey', 'inventory', 'document', 'recon'],
|
|
2180
|
+
reviewer: ['review', 'security', 'audit', 'test', 'quality', 'check', 'verify',
|
|
2181
|
+
'validate', 'pr', 'pull', 'lint', 'coverage', 'performance', 'checklist'],
|
|
2182
|
+
coordinator: ['decompose', 'plan', 'coordinate', 'parallel', 'orchestrate', 'manage',
|
|
2183
|
+
'delegate', 'assign', 'breakdown', 'workflow', 'group', 'subtask'],
|
|
2184
|
+
}
|
|
2185
|
+
// Skills that don't match any role-specific keyword are treated as universal
|
|
2186
|
+
// (e.g. 'session-start', 'workflow-guide') and given to all roles.
|
|
2187
|
+
const ALL_ROLE_KEYWORDS = Object.values(ROLE_SKILL_KEYWORDS).flat()
|
|
2188
|
+
const isRoleSpecific = (s) => ALL_ROLE_KEYWORDS.some(kw => `${s.name} ${s.description || ''}`.toLowerCase().includes(kw))
|
|
2189
|
+
|
|
2190
|
+
const roleKeywords = effectiveRole ? (ROLE_SKILL_KEYWORDS[effectiveRole] || []) : []
|
|
2191
|
+
|
|
2192
|
+
// Also use the task type's suggestedSkills list for relevance
|
|
2193
|
+
const taskTypeSkills = (() => {
|
|
2194
|
+
try {
|
|
2195
|
+
const taskType = ctx.task?.taskType || 'feature'
|
|
2196
|
+
const cfg = TASK_TYPES?.[taskType]
|
|
2197
|
+
return cfg?.suggestedSkills || []
|
|
2198
|
+
} catch { return [] }
|
|
2199
|
+
})()
|
|
2200
|
+
|
|
2201
|
+
const relevantSkills = enabledSkills.filter(s => {
|
|
2202
|
+
// Always include explicitly overridden-true skills (task-level config)
|
|
2203
|
+
if (taskSkillOverrides[s.name] === true) return true
|
|
2204
|
+
// Always include task-type suggested skills
|
|
2205
|
+
if (taskTypeSkills.includes(s.name)) return true
|
|
2206
|
+
// Include universal skills (don't match any role's keywords — generic helpers)
|
|
2207
|
+
if (!isRoleSpecific(s)) return true
|
|
2208
|
+
// Include if skill name/description matches this role's keywords
|
|
2209
|
+
const haystack = `${s.name} ${s.description || ''}`.toLowerCase()
|
|
2210
|
+
return roleKeywords.some(kw => haystack.includes(kw))
|
|
2211
|
+
})
|
|
2212
|
+
|
|
2213
|
+
const suggestedSkills = relevantSkills.length > 0
|
|
2214
|
+
? relevantSkills.map(s => ({
|
|
1925
2215
|
name: s.name,
|
|
1926
2216
|
description: s.description,
|
|
1927
2217
|
path: `.cursor/skills/${s.name}.md`,
|
|
@@ -1943,7 +2233,12 @@ Returns systemPrompt ready to use as a Claude system prompt.`,
|
|
|
1943
2233
|
suggestedSkills,
|
|
1944
2234
|
task: ctx.task,
|
|
1945
2235
|
project: ctx.project,
|
|
1946
|
-
usage:
|
|
2236
|
+
usage: [
|
|
2237
|
+
'Use systemPrompt as your Claude system prompt.',
|
|
2238
|
+
'If allowedTools is set, restrict your MCP tool calls to that list.',
|
|
2239
|
+
'Read ONLY the skills listed in suggestedSkills — they are filtered for your role and task type.',
|
|
2240
|
+
'After the task is done (park_task or raise_pr), the skill files are automatically deleted from .cursor/skills/.',
|
|
2241
|
+
].join(' '),
|
|
1947
2242
|
})
|
|
1948
2243
|
}
|
|
1949
2244
|
)
|
|
@@ -2078,9 +2373,14 @@ Returns reviewId — save it and pass it to merge_pr to prove semantic review ha
|
|
|
2078
2373
|
if (!prNumber) return errorText('No PR linked to this task. Call raise_pr first.')
|
|
2079
2374
|
|
|
2080
2375
|
// Enforce that analysisPoints are specific — reject obviously generic ones
|
|
2081
|
-
const genericPhrases = [
|
|
2376
|
+
const genericPhrases = [
|
|
2377
|
+
'looks good', 'lgtm', 'code is fine', 'no issues', 'all good', 'seems correct',
|
|
2378
|
+
'looks great', 'looks fine', 'all correct', 'perfect', 'seems fine', 'this is fine',
|
|
2379
|
+
'this is correct', 'no problems', 'nothing wrong', 'good job', 'well done',
|
|
2380
|
+
'approved', 'ship it', 'looks right', 'seems right', 'everything is fine',
|
|
2381
|
+
]
|
|
2082
2382
|
const tooGeneric = analysisPoints.filter(p =>
|
|
2083
|
-
p.trim().length <
|
|
2383
|
+
p.trim().length < 25 || genericPhrases.some(g => p.toLowerCase().includes(g))
|
|
2084
2384
|
)
|
|
2085
2385
|
if (tooGeneric.length > 0) {
|
|
2086
2386
|
return text({
|
|
@@ -2224,8 +2524,15 @@ The task moves to Done automatically via the GitHub webhook.`,
|
|
|
2224
2524
|
{ check: `CI checks (${checks?.total ?? 0} runs)`,
|
|
2225
2525
|
pass: skipChecks ? null : (checks?.allPassed ?? null),
|
|
2226
2526
|
note: skipChecks ? 'Skipped by request' : checks?.anyFailed ? 'Some checks failed' : checks?.allPassed ? 'All passing' : 'No checks found' },
|
|
2227
|
-
|
|
2228
|
-
|
|
2527
|
+
// Hard block: PR must have at least one GitHub approval OR a sessionReview from this session.
|
|
2528
|
+
// A sessionReview alone (reviewer = author) is not enough — someone independent must approve.
|
|
2529
|
+
// Exception: if there's a sessionReview AND existing approvals, both gates are satisfied.
|
|
2530
|
+
{ check: 'Has at least one GitHub approval', pass: pr.approvals > 0,
|
|
2531
|
+
note: pr.approvals === 0
|
|
2532
|
+
? hasSessionReview
|
|
2533
|
+
? '⚠️ Self-review only — no independent GitHub approval. Ask a teammate to approve on GitHub before merging.'
|
|
2534
|
+
: 'No approvals. Call review_pr → post_pr_review, then ask a reviewer to approve on GitHub.'
|
|
2535
|
+
: `${pr.approvals} GitHub approval(s)` },
|
|
2229
2536
|
]
|
|
2230
2537
|
|
|
2231
2538
|
const hardBlocks = safetyChecks.filter(c => c.pass === false && c.check !== `CI checks (${checks?.total ?? 0} runs)`)
|
|
@@ -2274,14 +2581,17 @@ The task moves to Done automatically via the GitHub webhook.`,
|
|
|
2274
2581
|
return text({ merged: true, message: 'PR was already merged.' })
|
|
2275
2582
|
}
|
|
2276
2583
|
|
|
2584
|
+
// Move task to done immediately — don't rely solely on the GitHub webhook
|
|
2585
|
+
try { await api.post(`/api/tasks/${taskId}/move`, { column: 'done', toIndex: 0 }) } catch { /* webhook fallback */ }
|
|
2586
|
+
|
|
2277
2587
|
return text({
|
|
2278
2588
|
merged: true,
|
|
2279
2589
|
sha: mergeRes.data?.sha,
|
|
2280
2590
|
prNumber,
|
|
2281
2591
|
taskKey: task.key,
|
|
2282
2592
|
mergeMethod,
|
|
2283
|
-
message: `✅ PR #${prNumber} merged via ${mergeMethod}. Task "${task.key}"
|
|
2284
|
-
nextStep: `
|
|
2593
|
+
message: `✅ PR #${prNumber} merged via ${mergeMethod}. Task "${task.key}" moved to Done.`,
|
|
2594
|
+
nextStep: `Run deleteCursorWorkspace cleanup if any agent files remain, then pick up your next task.`,
|
|
2285
2595
|
})
|
|
2286
2596
|
}
|
|
2287
2597
|
)
|
|
@@ -2482,6 +2792,34 @@ Flow:
|
|
|
2482
2792
|
api.patch(`/api/tasks/${taskId}`, { agentRole }).catch(() => {/* non-fatal */})
|
|
2483
2793
|
}
|
|
2484
2794
|
|
|
2795
|
+
// Re-write full cursor workspace (skills, session protocol, active-agent)
|
|
2796
|
+
// resume_task is equivalent to unpark_task for workspace purposes.
|
|
2797
|
+
let workspaceResult = null
|
|
2798
|
+
try {
|
|
2799
|
+
let projectAgentConfig = null
|
|
2800
|
+
try {
|
|
2801
|
+
const projRes = await api.get(`/api/projects/${task.project}`)
|
|
2802
|
+
if (projRes?.success) projectAgentConfig = projRes.data.project?.agentConfig || null
|
|
2803
|
+
} catch { /* non-fatal */ }
|
|
2804
|
+
|
|
2805
|
+
const taskForWorkspace = agentRole ? { ...task, agentRole } : task
|
|
2806
|
+
workspaceResult = writeCursorWorkspace(taskForWorkspace, projectAgentConfig, repoPath || process.cwd())
|
|
2807
|
+
|
|
2808
|
+
// Log injected files to session timeline
|
|
2809
|
+
const writtenFiles = workspaceResult?.written || []
|
|
2810
|
+
for (const filePath of writtenFiles) {
|
|
2811
|
+
const fileName = filePath.split('/').pop().replace(/\.(md|mdc)$/, '')
|
|
2812
|
+
const isSkill = filePath.includes('/.cursor/skills/')
|
|
2813
|
+
const isRule = filePath.includes('/.cursor/rules/')
|
|
2814
|
+
const isAgent = filePath.includes('/.cursor/agents/')
|
|
2815
|
+
const type = isSkill ? 'skill' : isRule ? 'rule' : isAgent ? 'subagent' : 'info'
|
|
2816
|
+
api.post(`/api/tasks/${taskId}/session/log`, {
|
|
2817
|
+
type, name: fileName, role: agentRole || null,
|
|
2818
|
+
summary: `📖 Injected at resume: ${filePath.replace(workspaceResult.repoRoot, '')}`,
|
|
2819
|
+
}).catch(() => {})
|
|
2820
|
+
}
|
|
2821
|
+
} catch { /* non-fatal */ }
|
|
2822
|
+
|
|
2485
2823
|
// Last commit metadata
|
|
2486
2824
|
const lastCommit = getLastCommitMeta(repoRoot)
|
|
2487
2825
|
|
|
@@ -2497,11 +2835,20 @@ Flow:
|
|
|
2497
2835
|
cursorRules: cursorRulesFile
|
|
2498
2836
|
? { restored: true, path: cursorRulesFile, agentRole: agentRole || null }
|
|
2499
2837
|
: { restored: false },
|
|
2838
|
+
workspace: workspaceResult
|
|
2839
|
+
? { restored: true, filesCount: workspaceResult.written?.length || 0, files: workspaceResult.written || [] }
|
|
2840
|
+
: { restored: false },
|
|
2841
|
+
previouslyClaimed: task?.claimedFiles?.length ? task.claimedFiles : null,
|
|
2500
2842
|
message: gitResult?.switched || gitResult?.alreadyOnBranch
|
|
2501
|
-
? `You are on branch "${branch}".
|
|
2843
|
+
? `You are on branch "${branch}". Cursor workspace restored${agentRole ? ` as ${agentRole}` : ''}. Read all skills before editing.`
|
|
2502
2844
|
: `Branch switch failed — see git.manualSteps.`,
|
|
2503
2845
|
nextStep: agentRole === 'builder'
|
|
2504
|
-
?
|
|
2846
|
+
? [
|
|
2847
|
+
`BUILDER role active.`,
|
|
2848
|
+
`1. ⚠️ READ all injected skills (workspace.files) — mandatory before any edits`,
|
|
2849
|
+
`2. Re-claim files: claim_files(taskId="${taskId}", files=${JSON.stringify(task?.claimedFiles || [])})`,
|
|
2850
|
+
`3. Continue implementation from where you left off`,
|
|
2851
|
+
].join('\n')
|
|
2505
2852
|
: agentRole === 'scout'
|
|
2506
2853
|
? `SCOUT role active. Read-only mode — map the codebase and save findings with update_task(scoutReport=...).`
|
|
2507
2854
|
: agentRole === 'coordinator'
|
|
@@ -2511,8 +2858,8 @@ Flow:
|
|
|
2511
2858
|
: task.column === 'in_review'
|
|
2512
2859
|
? `Task is in review. Check if PR feedback needs addressing — call fix_pr_feedback if needed.`
|
|
2513
2860
|
: task.parkNote?.remaining
|
|
2514
|
-
? `Remaining: ${task.parkNote.remaining}
|
|
2515
|
-
: `Continue coding on "${branch}".`,
|
|
2861
|
+
? `Remaining: ${task.parkNote.remaining}. Read workspace.files skills first.`
|
|
2862
|
+
: `Continue coding on "${branch}". Read workspace.files skills first.`,
|
|
2516
2863
|
})
|
|
2517
2864
|
}
|
|
2518
2865
|
)
|
|
@@ -2580,21 +2927,36 @@ The agent files are fully self-contained: each builder knows exactly what MCP to
|
|
|
2580
2927
|
const subtasks = dec.subtasks || []
|
|
2581
2928
|
const execOrder = dec.executionOrder || []
|
|
2582
2929
|
|
|
2583
|
-
//
|
|
2584
|
-
// subtask
|
|
2930
|
+
// Dual-signal completion check — mirrors wait_for_group logic.
|
|
2931
|
+
// A subtask is "done" if EITHER the parent checklist ticks it OR its child task
|
|
2932
|
+
// column is done/in_review. This prevents re-emitting builder prompts for subtasks
|
|
2933
|
+
// that are already finished but whose builder skipped update_task on the parent.
|
|
2585
2934
|
const boardSubtasks = task.subtasks || []
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
const
|
|
2935
|
+
const checklistDone = (title) => boardSubtasks.some(s => s.done && (s.title === title || s.title.endsWith(title)))
|
|
2936
|
+
const childColumnDoneCache = {}
|
|
2937
|
+
const isSubtaskDone = async (title) => {
|
|
2938
|
+
if (checklistDone(title)) return true
|
|
2939
|
+
const entry = subtasks.find(s => s.title === title)
|
|
2940
|
+
if (!entry?.childTaskId) return false
|
|
2941
|
+
if (childColumnDoneCache[entry.childTaskId] !== undefined) return childColumnDoneCache[entry.childTaskId]
|
|
2942
|
+
try {
|
|
2943
|
+
const r = await api.get(`/api/tasks/${entry.childTaskId}`)
|
|
2944
|
+
const col = r?.data?.task?.column
|
|
2945
|
+
const done = col === 'done' || col === 'in_review'
|
|
2946
|
+
childColumnDoneCache[entry.childTaskId] = done
|
|
2947
|
+
return done
|
|
2948
|
+
} catch { return false }
|
|
2949
|
+
}
|
|
2589
2950
|
|
|
2590
2951
|
let nextGroup = null
|
|
2591
2952
|
let nextGroupIndex = -1
|
|
2592
2953
|
for (let i = 0; i < execOrder.length; i++) {
|
|
2593
2954
|
const group = execOrder[i]
|
|
2594
|
-
const allDone = group.
|
|
2955
|
+
const allDone = (await Promise.all(group.map(t => isSubtaskDone(t)))).every(Boolean)
|
|
2595
2956
|
if (!allDone) {
|
|
2596
2957
|
// Also check all prior groups are fully done (respects dependsOn)
|
|
2597
|
-
const
|
|
2958
|
+
const priorResults = await Promise.all(execOrder.slice(0, i).map(g => Promise.all(g.map(t => isSubtaskDone(t)))))
|
|
2959
|
+
const priorAllDone = priorResults.every(g => g.every(Boolean))
|
|
2598
2960
|
if (priorAllDone) {
|
|
2599
2961
|
nextGroup = group
|
|
2600
2962
|
nextGroupIndex = i + 1
|
|
@@ -2610,8 +2972,9 @@ The agent files are fully self-contained: each builder knows exactly what MCP to
|
|
|
2610
2972
|
})
|
|
2611
2973
|
}
|
|
2612
2974
|
|
|
2613
|
-
// Only kick off subtasks that aren't done yet
|
|
2614
|
-
const pendingInGroup = nextGroup.
|
|
2975
|
+
// Only kick off subtasks that aren't done yet (use cached dual-signal results)
|
|
2976
|
+
const pendingInGroup = (await Promise.all(nextGroup.map(async t => ({ t, done: await isSubtaskDone(t) }))))
|
|
2977
|
+
.filter(r => !r.done).map(r => r.t)
|
|
2615
2978
|
const parallelCount = pendingInGroup.length
|
|
2616
2979
|
const isParallel = parallelCount > 1
|
|
2617
2980
|
|
|
@@ -2629,35 +2992,42 @@ The agent files are fully self-contained: each builder knows exactly what MCP to
|
|
|
2629
2992
|
const kickoffs = pendingInGroup.map((subtaskTitle, i) => {
|
|
2630
2993
|
const st = subtasks.find(s => s.title === subtaskTitle) || { title: subtaskTitle, role: 'builder', files: [], description: '' }
|
|
2631
2994
|
const filesArg = (st.files || []).map(f => `"${f}"`).join(', ')
|
|
2632
|
-
const fileList = (st.files || []).map(f => ` - \`${f}\``).join('\n')
|
|
2633
|
-
|
|
2634
|
-
// Prompt is a short, direct imperative Cursor immediately executes.
|
|
2635
|
-
// Key: start with "Use the InternalTool MCP" so Cursor knows to call tools.
|
|
2636
|
-
// Avoid pseudocode like fn(arg=val) — use plain English with quoted values.
|
|
2637
2995
|
const fileListPlain = (st.files || []).join(', ')
|
|
2996
|
+
|
|
2997
|
+
// Use the child task ID if it was embedded at decompose time.
|
|
2998
|
+
// Builders must work on their own child task, not the parent task.
|
|
2999
|
+
// The parent task ID is only used to update the parent checklist when done.
|
|
3000
|
+
const childTaskId = st.childTaskId || taskId // fallback to parent if missing (old decompositions)
|
|
3001
|
+
const childTaskKey = st.childTaskKey || task.key
|
|
3002
|
+
const usingChildTask = !!(st.childTaskId)
|
|
3003
|
+
|
|
2638
3004
|
const prompt = [
|
|
2639
|
-
`Use the InternalTool MCP tools to implement subtask "${st.title}"
|
|
3005
|
+
`Use the InternalTool MCP tools to implement subtask "${st.title}".`,
|
|
3006
|
+
`Your task: ${childTaskKey} (child of ${task.key})`,
|
|
3007
|
+
usingChildTask ? `Child task ID: "${childTaskId}" — use this for ALL MCP calls below.` : `⚠️ No child task ID found — using parent task ID "${taskId}" as fallback.`,
|
|
2640
3008
|
``,
|
|
2641
3009
|
`Do these steps immediately in order — do not ask for confirmation, do not skip any step:`,
|
|
2642
3010
|
``,
|
|
2643
|
-
`1. Call log_session_event with taskId "${
|
|
2644
|
-
`2. Call kickoff_task with taskId "${
|
|
2645
|
-
`
|
|
2646
|
-
`
|
|
3011
|
+
`1. Call log_session_event with taskId "${childTaskId}", type "subagent", name "builder-${i + 1}", summary "Builder ${i + 1} started: ${st.title}"`,
|
|
3012
|
+
`2. Call kickoff_task with taskId "${childTaskId}", agentRole "builder", confirmed true`,
|
|
3013
|
+
` (This writes your cursor workspace. READ every file in .cursor/skills/ before editing anything.)`,
|
|
3014
|
+
`3. Call claim_files with taskId "${childTaskId}" and files [${filesArg}]`,
|
|
3015
|
+
`4. Call get_agent_context with taskId "${childTaskId}" and role "builder" — read the full plan and skills`,
|
|
2647
3016
|
`5. Implement the subtask: ${st.description || st.title}`,
|
|
2648
3017
|
` - You may ONLY modify these files: ${fileListPlain}`,
|
|
2649
|
-
` - Do NOT touch any other file`,
|
|
2650
|
-
` - Follow
|
|
2651
|
-
`6. Run
|
|
2652
|
-
`7. Call commit_helper with taskId "${
|
|
2653
|
-
`8. Call
|
|
2654
|
-
`9. Call
|
|
2655
|
-
`10. Call log_session_event with taskId "${
|
|
3018
|
+
` - Do NOT touch any other file — other builders own those`,
|
|
3019
|
+
` - Follow skills listed in get_agent_context`,
|
|
3020
|
+
`6. Run the project test suite — all tests must pass before continuing`,
|
|
3021
|
+
`7. Call commit_helper with taskId "${childTaskId}"`,
|
|
3022
|
+
`8. Call raise_pr with taskId "${childTaskId}" and projectId "${task.project?._id || task.project}"`,
|
|
3023
|
+
`9. Call update_task with taskId "${taskId}" (parent) and mark the subtask "${st.title}" as done in the subtasks checklist`,
|
|
3024
|
+
`10. Call log_session_event with taskId "${childTaskId}", type "subagent", name "builder-${i + 1}", summary "Builder ${i + 1} finished: ${st.title}"`,
|
|
2656
3025
|
``,
|
|
2657
|
-
`You are builder ${i + 1} of ${parallelCount} running in parallel.
|
|
3026
|
+
`You are builder ${i + 1} of ${parallelCount} running in parallel. Other builders are working on different files simultaneously — do NOT touch their files.`,
|
|
3027
|
+
`If you finish early, call park_task on your child task and notify the coordinator.`,
|
|
2658
3028
|
].join('\n')
|
|
2659
3029
|
|
|
2660
|
-
return { subtask: st.title, role: st.role || 'builder', files: st.files || [], prompt }
|
|
3030
|
+
return { subtask: st.title, role: st.role || 'builder', files: st.files || [], childTaskId, childTaskKey, prompt }
|
|
2661
3031
|
})
|
|
2662
3032
|
|
|
2663
3033
|
// Write each builder prompt as a Cursor Background Agent file in .cursor/agents/
|
|
@@ -2674,14 +3044,14 @@ The agent files are fully self-contained: each builder knows exactly what MCP to
|
|
|
2674
3044
|
|
|
2675
3045
|
const agentFileContent = [
|
|
2676
3046
|
`---`,
|
|
2677
|
-
`description: Builder ${i + 1} of ${parallelCount} — ${k.subtask} (${task.key})`,
|
|
3047
|
+
`description: Builder ${i + 1} of ${parallelCount} — ${k.subtask} (${k.childTaskKey || task.key})`,
|
|
2678
3048
|
`---`,
|
|
2679
3049
|
``,
|
|
2680
3050
|
k.prompt,
|
|
2681
3051
|
].join('\n')
|
|
2682
3052
|
|
|
2683
3053
|
writeFileSync(filePath, agentFileContent, 'utf8')
|
|
2684
|
-
writtenFiles.push({ index: i + 1, file: `.cursor/agents/${fileName}`, subtask: k.subtask })
|
|
3054
|
+
writtenFiles.push({ index: i + 1, file: `.cursor/agents/${fileName}`, subtask: k.subtask, childTaskId: k.childTaskId, childTaskKey: k.childTaskKey })
|
|
2685
3055
|
})
|
|
2686
3056
|
|
|
2687
3057
|
// Build the human-facing instruction block
|
|
@@ -2693,13 +3063,11 @@ The agent files are fully self-contained: each builder knows exactly what MCP to
|
|
|
2693
3063
|
` 2. You'll see ${parallelCount} new builder agent(s) listed`,
|
|
2694
3064
|
` 3. Click "Start" for each one — they run in parallel automatically`,
|
|
2695
3065
|
``,
|
|
2696
|
-
...writtenFiles.map(f => ` • Builder ${f.index}: "${f.subtask}" → ${f.file}`),
|
|
3066
|
+
...writtenFiles.map(f => ` • Builder ${f.index}: "${f.subtask}" [${f.childTaskKey || 'parent'}] → ${f.file}`),
|
|
2697
3067
|
``,
|
|
2698
|
-
`Each builder
|
|
3068
|
+
`Each builder kicks off their own child task → claims files → implements → tests → commits → raises PR.`,
|
|
2699
3069
|
`They work on different files simultaneously — no need to wait for one before starting the other.`,
|
|
2700
|
-
|
|
2701
|
-
`After all builders finish, come back here and call get_parallel_kickoffs again`,
|
|
2702
|
-
`to get the next group's prompts.`,
|
|
3070
|
+
`When all builders in this group are done, call get_parallel_kickoffs again for the next group.`,
|
|
2703
3071
|
].join('\n')
|
|
2704
3072
|
|
|
2705
3073
|
return text({
|
|
@@ -2738,6 +3106,23 @@ Polls every 10 s, times out after 30 min by default. Returns immediately if alre
|
|
|
2738
3106
|
const deadline = Date.now() + timeoutMs
|
|
2739
3107
|
let attempts = 0
|
|
2740
3108
|
|
|
3109
|
+
// Helper: check if a subtask title is done via parent checklist
|
|
3110
|
+
const isCheckedDone = (boardSubtasks, title) =>
|
|
3111
|
+
boardSubtasks.some(s => s.done && (s.title === title || s.title.endsWith(title)))
|
|
3112
|
+
|
|
3113
|
+
// Helper: check if a child task is done via its board column.
|
|
3114
|
+
// A child task is considered done when it reaches 'done' or 'in_review'
|
|
3115
|
+
// (builder finished + raised PR). This is the reliable signal — builders
|
|
3116
|
+
// may skip calling update_task on the parent but they always raise_pr.
|
|
3117
|
+
const isChildTaskDone = async (childTaskId) => {
|
|
3118
|
+
if (!childTaskId) return false
|
|
3119
|
+
try {
|
|
3120
|
+
const res = await api.get(`/api/tasks/${childTaskId}`)
|
|
3121
|
+
const col = res?.data?.task?.column
|
|
3122
|
+
return col === 'done' || col === 'in_review'
|
|
3123
|
+
} catch { return false }
|
|
3124
|
+
}
|
|
3125
|
+
|
|
2741
3126
|
while (Date.now() < deadline) {
|
|
2742
3127
|
attempts++
|
|
2743
3128
|
const taskRes = await apiWithRetry(() => api.get(`/api/tasks/${taskId}`))
|
|
@@ -2750,27 +3135,43 @@ Polls every 10 s, times out after 30 min by default. Returns immediately if alre
|
|
|
2750
3135
|
return errorText('Decomposition JSON is malformed.')
|
|
2751
3136
|
}
|
|
2752
3137
|
|
|
2753
|
-
const execOrder
|
|
3138
|
+
const execOrder = dec.executionOrder || []
|
|
3139
|
+
const decSubtasks = dec.subtasks || []
|
|
2754
3140
|
const targetGroup = execOrder[groupIndex - 1]
|
|
2755
3141
|
if (!targetGroup) return errorText(`Group ${groupIndex} does not exist. Decomposition has ${execOrder.length} group(s).`)
|
|
2756
3142
|
|
|
2757
3143
|
const boardSubtasks = task.subtasks || []
|
|
2758
|
-
const isDone = (title) => boardSubtasks.some(s => s.done && (s.title === title || s.title.endsWith(title)))
|
|
2759
3144
|
|
|
2760
|
-
|
|
2761
|
-
|
|
3145
|
+
// Dual-signal completion: parent checklist tick OR child task column (done/in_review).
|
|
3146
|
+
// This prevents the coordinator from stalling when a builder skips update_task on the parent.
|
|
3147
|
+
const statusPerTitle = await Promise.all(
|
|
3148
|
+
targetGroup.map(async (title) => {
|
|
3149
|
+
const checklist = isCheckedDone(boardSubtasks, title)
|
|
3150
|
+
if (checklist) return { title, done: true, via: 'checklist' }
|
|
3151
|
+
// Look up child task ID from the embedded decomposition
|
|
3152
|
+
const decEntry = decSubtasks.find(s => s.title === title)
|
|
3153
|
+
const childDone = decEntry?.childTaskId
|
|
3154
|
+
? await isChildTaskDone(decEntry.childTaskId)
|
|
3155
|
+
: false
|
|
3156
|
+
return { title, done: childDone, via: childDone ? 'child_task_column' : 'pending', childTaskId: decEntry?.childTaskId || null }
|
|
3157
|
+
})
|
|
3158
|
+
)
|
|
3159
|
+
|
|
3160
|
+
const doneTitles = statusPerTitle.filter(s => s.done)
|
|
3161
|
+
const pendingTitles = statusPerTitle.filter(s => !s.done)
|
|
2762
3162
|
|
|
2763
3163
|
if (pendingTitles.length === 0) {
|
|
2764
3164
|
return text({
|
|
2765
|
-
done:
|
|
2766
|
-
group:
|
|
2767
|
-
subtasks:
|
|
3165
|
+
done: true,
|
|
3166
|
+
group: groupIndex,
|
|
3167
|
+
subtasks: targetGroup,
|
|
2768
3168
|
done_count: doneTitles.length,
|
|
3169
|
+
completion: doneTitles.map(s => ({ title: s.title, via: s.via })),
|
|
2769
3170
|
attempts,
|
|
2770
3171
|
message: `✅ All ${targetGroup.length} subtask(s) in Group ${groupIndex} are done after ${attempts} poll(s).`,
|
|
2771
3172
|
nextStep: groupIndex < execOrder.length
|
|
2772
3173
|
? `Call get_parallel_kickoffs with taskId="${taskId}" to get Group ${groupIndex + 1}'s builder prompts.`
|
|
2773
|
-
: 'All groups complete. Run final tests then call raise_pr.',
|
|
3174
|
+
: 'All groups complete. Run final tests then call raise_pr on the parent task.',
|
|
2774
3175
|
})
|
|
2775
3176
|
}
|
|
2776
3177
|
|
|
@@ -2793,10 +3194,17 @@ Polls every 10 s, times out after 30 min by default. Returns immediately if alre
|
|
|
2793
3194
|
const finalTask = finalRes?.data?.task
|
|
2794
3195
|
let finalDec
|
|
2795
3196
|
try { finalDec = JSON.parse(finalTask?.decomposition || '{}') } catch { finalDec = {} }
|
|
2796
|
-
const finalGroup
|
|
2797
|
-
const
|
|
2798
|
-
const
|
|
2799
|
-
const stillPending
|
|
3197
|
+
const finalGroup = (finalDec.executionOrder || [])[groupIndex - 1] || []
|
|
3198
|
+
const finalDecSubs = finalDec.subtasks || []
|
|
3199
|
+
const finalBoard = finalTask?.subtasks || []
|
|
3200
|
+
const stillPending = await Promise.all(
|
|
3201
|
+
finalGroup.map(async (title) => {
|
|
3202
|
+
if (isCheckedDone(finalBoard, title)) return null
|
|
3203
|
+
const entry = finalDecSubs.find(s => s.title === title)
|
|
3204
|
+
if (entry?.childTaskId && await isChildTaskDone(entry.childTaskId)) return null
|
|
3205
|
+
return title
|
|
3206
|
+
})
|
|
3207
|
+
).then(r => r.filter(Boolean))
|
|
2800
3208
|
|
|
2801
3209
|
return text({
|
|
2802
3210
|
timedOut: true,
|
|
@@ -2804,7 +3212,7 @@ Polls every 10 s, times out after 30 min by default. Returns immediately if alre
|
|
|
2804
3212
|
pending: stillPending,
|
|
2805
3213
|
attempts,
|
|
2806
3214
|
message: `⏱ Timed out after ${attempts} poll(s). ${stillPending.length} subtask(s) still pending: ${stillPending.join(', ')}`,
|
|
2807
|
-
hint: 'Check the Background Agents panel — a builder may have crashed. Call wait_for_group again to resume
|
|
3215
|
+
hint: 'Check the Background Agents panel — a builder may have crashed or skipped raise_pr. Call wait_for_group again to resume, or proceed manually if the work is done.',
|
|
2808
3216
|
})
|
|
2809
3217
|
}
|
|
2810
3218
|
)
|
|
@@ -3626,6 +4034,18 @@ Use this when a developer says "start task", "brief me on", or "what do I need t
|
|
|
3626
4034
|
const hasParallelLang = /parallel|simultaneously|independent(ly)?|at the same time/i.test(readme)
|
|
3627
4035
|
const hasSequentialLang = /step\s*\d|first[\s,].*then|depends\s+on|before.*after/i.test(readme)
|
|
3628
4036
|
|
|
4037
|
+
// Count how many distinct codebase layers are touched: routes, models, components, services, tests, migrations, seeds, utils, middleware, hooks
|
|
4038
|
+
const LAYER_PATTERNS = ['route', 'model', 'component', 'service', 'test', 'migration', 'seed', 'util', 'middleware', 'hook', 'schema', 'store', 'context', 'controller', 'resolver']
|
|
4039
|
+
const layersHit = LAYER_PATTERNS.filter(l => new RegExp(l, 'i').test(readme)).length
|
|
4040
|
+
|
|
4041
|
+
// Count explicit numbered steps / subtasks in the plan (indicates decomposability)
|
|
4042
|
+
const numberedSteps = (readme.match(/^\s*\d+\.\s/gm) || []).length
|
|
4043
|
+
|
|
4044
|
+
// Full-stack scope: touches both frontend and backend layers
|
|
4045
|
+
const hasFrontend = /component|hook|store|context|jsx|tsx|vue|react|svelte|ui|page|view/i.test(readme)
|
|
4046
|
+
const hasBackend = /route|controller|model|schema|service|middleware|api|endpoint|database|migration/i.test(readme)
|
|
4047
|
+
const isFullStack = hasFrontend && hasBackend
|
|
4048
|
+
|
|
3629
4049
|
if (readmeLen > 800) { complexityScore += 2; complexitySignals.push(`long plan (${readmeLen} chars)`) }
|
|
3630
4050
|
else if (readmeLen > 300) { complexityScore += 1; complexitySignals.push(`medium plan (${readmeLen} chars)`) }
|
|
3631
4051
|
if (sections > 3) { complexityScore += 2; complexitySignals.push(`${sections} README sections`) }
|
|
@@ -3634,13 +4054,23 @@ Use this when a developer says "start task", "brief me on", or "what do I need t
|
|
|
3634
4054
|
else if (fileMentions > 2) { complexityScore += 1; complexitySignals.push(`${fileMentions} files mentioned`) }
|
|
3635
4055
|
if (hasParallelLang) { complexityScore += 1; complexitySignals.push('parallel workstreams mentioned') }
|
|
3636
4056
|
if (hasSequentialLang) { complexityScore += 1; complexitySignals.push('sequential steps mentioned') }
|
|
4057
|
+
if (layersHit >= 4) { complexityScore += 2; complexitySignals.push(`${layersHit} codebase layers touched`) }
|
|
4058
|
+
else if (layersHit >= 2) { complexityScore += 1; complexitySignals.push(`${layersHit} codebase layers touched`) }
|
|
4059
|
+
if (numberedSteps >= 5) { complexityScore += 2; complexitySignals.push(`${numberedSteps} explicit steps in plan`) }
|
|
4060
|
+
else if (numberedSteps >= 3) { complexityScore += 1; complexitySignals.push(`${numberedSteps} steps in plan`) }
|
|
4061
|
+
if (isFullStack) { complexityScore += 1; complexitySignals.push('full-stack scope (frontend + backend)') }
|
|
3637
4062
|
}
|
|
3638
4063
|
|
|
4064
|
+
// Score from task-level signals (subtask count from board)
|
|
4065
|
+
const existingSubtaskCount = (task.subtasks || []).length
|
|
4066
|
+
if (existingSubtaskCount >= 5) { complexityScore += 2; complexitySignals.push(`${existingSubtaskCount} subtasks already defined`) }
|
|
4067
|
+
else if (existingSubtaskCount >= 3) { complexityScore += 1; complexitySignals.push(`${existingSubtaskCount} subtasks defined`) }
|
|
4068
|
+
|
|
3639
4069
|
const titleLower = task.title.toLowerCase()
|
|
3640
|
-
const complexTitleWords = ['implement', 'build', 'add', 'create', 'integrate', 'refactor', 'migrate', 'redesign']
|
|
3641
|
-
const simpleTitleWords = ['fix', 'patch', 'typo', 'bump', 'revert']
|
|
3642
|
-
if (complexTitleWords.some(w => titleLower.includes(w))) complexityScore += 1
|
|
3643
|
-
if (simpleTitleWords.some(w => titleLower.includes(w))) complexityScore -= 1
|
|
4070
|
+
const complexTitleWords = ['implement', 'build', 'add', 'create', 'integrate', 'refactor', 'migrate', 'redesign', 'replace', 'overhaul', 'rearchitect']
|
|
4071
|
+
const simpleTitleWords = ['fix', 'patch', 'typo', 'bump', 'revert', 'update', 'rename', 'remove']
|
|
4072
|
+
if (complexTitleWords.some(w => titleLower.includes(w))) { complexityScore += 1; complexitySignals.push('complex verb in title') }
|
|
4073
|
+
if (simpleTitleWords.some(w => titleLower.includes(w))) { complexityScore -= 1 }
|
|
3644
4074
|
complexityScore = Math.max(0, complexityScore)
|
|
3645
4075
|
|
|
3646
4076
|
// ── Determine recommended flow ────────────────────────────────────────────
|
|
@@ -3669,6 +4099,64 @@ Use this when a developer says "start task", "brief me on", or "what do I need t
|
|
|
3669
4099
|
const subtasksDone = subtasks.filter(s => s.done).length
|
|
3670
4100
|
const subtasksTotal = subtasks.length
|
|
3671
4101
|
|
|
4102
|
+
// ── Gather local git state (used in both preview and confirmed path) ────────
|
|
4103
|
+
let localGitState = null
|
|
4104
|
+
{
|
|
4105
|
+
const pCwd = repoPath || process.cwd()
|
|
4106
|
+
const pRoot = findRepoRoot(pCwd)
|
|
4107
|
+
if (pRoot) {
|
|
4108
|
+
try {
|
|
4109
|
+
const porcelain = runGit('status --porcelain=v1', pRoot)
|
|
4110
|
+
const { localState, modified, staged, unstaged, untracked } = parseGitStatus(porcelain)
|
|
4111
|
+
const currentBranch = runGit('branch --show-current', pRoot).trim()
|
|
4112
|
+
|
|
4113
|
+
// Detect default branch (main / master)
|
|
4114
|
+
let defaultBranch = 'main'
|
|
4115
|
+
try {
|
|
4116
|
+
defaultBranch = runGit('symbolic-ref refs/remotes/origin/HEAD', pRoot).trim().replace('refs/remotes/origin/', '')
|
|
4117
|
+
} catch { /* no remote HEAD — stay with 'main' */ }
|
|
4118
|
+
|
|
4119
|
+
// How far behind is the current branch from origin/<default>?
|
|
4120
|
+
let mainBehindBy = 0
|
|
4121
|
+
try {
|
|
4122
|
+
runGit('fetch origin --quiet', pRoot)
|
|
4123
|
+
mainBehindBy = parseInt(runGit(`rev-list HEAD..origin/${defaultBranch} --count`, pRoot).trim(), 10) || 0
|
|
4124
|
+
} catch { /* no remote or network — non-fatal */ }
|
|
4125
|
+
|
|
4126
|
+
localGitState = {
|
|
4127
|
+
repoRoot: pRoot,
|
|
4128
|
+
currentBranch,
|
|
4129
|
+
defaultBranch,
|
|
4130
|
+
localState, // 'clean' | 'modified' | 'untracked'
|
|
4131
|
+
modifiedFiles: modified,
|
|
4132
|
+
untrackedFiles: untracked,
|
|
4133
|
+
mainBehindBy,
|
|
4134
|
+
}
|
|
4135
|
+
} catch { /* git not available or not a git repo — non-fatal */ }
|
|
4136
|
+
}
|
|
4137
|
+
}
|
|
4138
|
+
|
|
4139
|
+
// ── Fetch team landscape for the preview so agent sees conflicts before committing ──
|
|
4140
|
+
let previewTeamContext = null
|
|
4141
|
+
try {
|
|
4142
|
+
const previewTreeRes = await api.get(`/api/projects/${task.project}/github/git-tree`).catch(() => null)
|
|
4143
|
+
if (previewTreeRes?.success) {
|
|
4144
|
+
const branches = previewTreeRes.data?.branches || []
|
|
4145
|
+
const otherActive = branches.filter(b => b.taskId !== String(task._id) && !b.branchError)
|
|
4146
|
+
if (otherActive.length > 0) {
|
|
4147
|
+
previewTeamContext = {
|
|
4148
|
+
activeBranches: otherActive.map(b => ({
|
|
4149
|
+
taskKey: b.taskKey,
|
|
4150
|
+
assignees: (b.assignees || []).map(a => a.name).join(', ') || 'unassigned',
|
|
4151
|
+
files: b.claimedFiles || [],
|
|
4152
|
+
behindBy: b.compare?.behindBy ?? null,
|
|
4153
|
+
})),
|
|
4154
|
+
warning: `⚠️ ${otherActive.length} other branch(es) active in this project. Check for file overlaps before claiming files.`,
|
|
4155
|
+
}
|
|
4156
|
+
}
|
|
4157
|
+
}
|
|
4158
|
+
} catch { /* non-fatal */ }
|
|
4159
|
+
|
|
3672
4160
|
// ── Preview: show the full plan before touching anything ──
|
|
3673
4161
|
const pendingApv = (task.approvals || []).find(a => a.state === 'pending') || null
|
|
3674
4162
|
const hasApprovedApv = (task.approvals || []).some(a => a.state === 'approved')
|
|
@@ -3887,10 +4375,25 @@ Use this when a developer says "start task", "brief me on", or "what do I need t
|
|
|
3887
4375
|
claimedFiles: task.claimedFiles || [],
|
|
3888
4376
|
agentRole: task.agentRole || null,
|
|
3889
4377
|
},
|
|
4378
|
+
localGitState: localGitState ? {
|
|
4379
|
+
currentBranch: localGitState.currentBranch,
|
|
4380
|
+
status: localGitState.localState,
|
|
4381
|
+
modifiedFiles: localGitState.modifiedFiles,
|
|
4382
|
+
untrackedFiles: localGitState.untrackedFiles,
|
|
4383
|
+
mainBehindBy: localGitState.mainBehindBy,
|
|
4384
|
+
...(localGitState.localState === 'modified' && {
|
|
4385
|
+
warning: `⚠️ You have ${localGitState.modifiedFiles.length} uncommitted change(s) on "${localGitState.currentBranch}". Commit or stash before starting a new task to avoid cross-task pollution.`,
|
|
4386
|
+
suggestedFix: `git stash push -m "wip: before starting ${task.key}"`,
|
|
4387
|
+
}),
|
|
4388
|
+
...(localGitState.mainBehindBy > 0 && {
|
|
4389
|
+
mainWarning: `⚠️ Your workspace is ${localGitState.mainBehindBy} commit(s) behind origin/${localGitState.defaultBranch}. Run: git pull origin ${localGitState.defaultBranch} before creating a branch.`,
|
|
4390
|
+
}),
|
|
4391
|
+
} : null,
|
|
4392
|
+
teamLandscape: previewTeamContext,
|
|
3890
4393
|
requiresConfirmation: true,
|
|
3891
4394
|
message: approvalBlocks
|
|
3892
4395
|
? `Read the plan above, then follow workflowRoadmap — approval is required before you can branch and start coding.`
|
|
3893
|
-
: `Read the implementation plan above carefully, then call kickoff_task again with confirmed=true.`,
|
|
4396
|
+
: `Read the implementation plan above carefully, resolve any localGitState warnings, then call kickoff_task again with confirmed=true.`,
|
|
3894
4397
|
})
|
|
3895
4398
|
}
|
|
3896
4399
|
|
|
@@ -3948,28 +4451,39 @@ Use this when a developer says "start task", "brief me on", or "what do I need t
|
|
|
3948
4451
|
}
|
|
3949
4452
|
|
|
3950
4453
|
// ── #1 Preflight: check dirty tree before writing cursor rules / moving task ──
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
const
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
4454
|
+
// Use the git state we already gathered above; re-check if not available.
|
|
4455
|
+
if (localGitState?.localState === 'modified') {
|
|
4456
|
+
const { currentBranch, modifiedFiles } = localGitState
|
|
4457
|
+
const taskBranch = task.github?.headBranch
|
|
4458
|
+
|
|
4459
|
+
if (taskBranch && currentBranch !== taskBranch) {
|
|
4460
|
+
// On the wrong branch — stash and switch, or cancel
|
|
4461
|
+
return text({
|
|
4462
|
+
blocked: true,
|
|
4463
|
+
reason: `You are on "${currentBranch}" with ${modifiedFiles.length} uncommitted change(s), but this task's branch is "${taskBranch}".`,
|
|
4464
|
+
modifiedFiles,
|
|
4465
|
+
choices: {
|
|
4466
|
+
stash: `git stash push -m "wip: before kickoff ${task.key} on ${currentBranch}"`,
|
|
4467
|
+
autoSwitch: `git stash && git checkout ${taskBranch} && git stash pop`,
|
|
4468
|
+
cancel: 'Review your changes first, then retry kickoff_task',
|
|
4469
|
+
},
|
|
4470
|
+
message: '⛔ Resolve the working tree before kicking off this task to avoid cross-task pollution.',
|
|
4471
|
+
})
|
|
4472
|
+
}
|
|
4473
|
+
|
|
4474
|
+
if (!taskBranch) {
|
|
4475
|
+
// First-time kickoff — uncommitted changes risk bleeding into the new branch
|
|
4476
|
+
return text({
|
|
4477
|
+
blocked: true,
|
|
4478
|
+
reason: `You have ${modifiedFiles.length} uncommitted change(s) on "${currentBranch}". Starting a new task from a dirty tree would carry these changes into the new branch.`,
|
|
4479
|
+
modifiedFiles,
|
|
4480
|
+
choices: {
|
|
4481
|
+
stash: `git stash push -m "wip: before starting ${task.key}"`,
|
|
4482
|
+
commit: 'git add -p && git commit -m "wip: save changes before new task"',
|
|
4483
|
+
cancel: 'Review your changes, then retry kickoff_task',
|
|
4484
|
+
},
|
|
4485
|
+
message: '⛔ Commit or stash your changes before creating a new task branch.',
|
|
4486
|
+
})
|
|
3973
4487
|
}
|
|
3974
4488
|
}
|
|
3975
4489
|
|
|
@@ -4145,6 +4659,26 @@ After \`request_human_input\`: STOP, show the question in chat, wait for reply,
|
|
|
4145
4659
|
}
|
|
4146
4660
|
api.patch(`/api/tasks/${taskId}`, workspacePatch).catch(() => {/* non-fatal */})
|
|
4147
4661
|
|
|
4662
|
+
// Auto-log every skill / rule / agent written to the session timeline
|
|
4663
|
+
// so the Session tab always shows what got injected at kickoff.
|
|
4664
|
+
if (workspaceResult?.written?.length) {
|
|
4665
|
+
const repoRoot = workspaceResult.repoRoot || ''
|
|
4666
|
+
for (const filePath of workspaceResult.written) {
|
|
4667
|
+
const rel = filePath.replace(repoRoot, '').replace(/^\//, '')
|
|
4668
|
+
const name = rel.replace(/^\.cursor\/(skills|rules|agents|commands)\//, '').replace(/\.(md|mdc)$/, '')
|
|
4669
|
+
const type = rel.includes('/skills/') ? 'skill'
|
|
4670
|
+
: rel.includes('/rules/') ? 'rule'
|
|
4671
|
+
: rel.includes('/agents/') ? 'subagent'
|
|
4672
|
+
: 'info'
|
|
4673
|
+
api.post(`/api/tasks/${taskId}/session/log`, {
|
|
4674
|
+
type,
|
|
4675
|
+
name,
|
|
4676
|
+
role: agentRole || null,
|
|
4677
|
+
summary: `${type === 'skill' ? '📖' : type === 'rule' ? '📏' : type === 'subagent' ? '🤖' : 'ℹ️'} Injected at kickoff: ${rel}`,
|
|
4678
|
+
}).catch(() => {/* non-fatal */})
|
|
4679
|
+
}
|
|
4680
|
+
}
|
|
4681
|
+
|
|
4148
4682
|
// Write .internaltool-active-task so the Claude Code hook knows a task is active
|
|
4149
4683
|
try {
|
|
4150
4684
|
writeFileSync(
|
|
@@ -4207,21 +4741,85 @@ After \`request_human_input\`: STOP, show the question in chat, wait for reply,
|
|
|
4207
4741
|
scoutFirst: taskTypeCfg.scoutFirst,
|
|
4208
4742
|
typeRulesInjected: !!typeExtraRules,
|
|
4209
4743
|
} : null,
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4744
|
+
mandatoryWorkflow: (() => {
|
|
4745
|
+
const flow = recommendedFlow // 'direct' | 'single_builder' | 'coordinator'
|
|
4746
|
+
const role = agentRole || 'builder'
|
|
4747
|
+
|
|
4748
|
+
// ── Coordinator flow ──────────────────────────────────────────────────
|
|
4749
|
+
if (role === 'coordinator') {
|
|
4750
|
+
return {
|
|
4751
|
+
flow: 'coordinator',
|
|
4752
|
+
WARNING: '⛔ COORDINATOR MODE — DO NOT WRITE ANY CODE. Your only job is to plan and delegate.',
|
|
4753
|
+
mandatorySteps: [
|
|
4754
|
+
'1. READ all files in .cursor/skills/ — log each with log_session_event(type="skill", name=<filename>)',
|
|
4755
|
+
'2. Call recall(taskId) to restore memory from previous sessions',
|
|
4756
|
+
'3. Call get_agent_context(taskId, role="coordinator") for the full context',
|
|
4757
|
+
`4. If no scout report exists: call scout_task(taskId="${taskId}", confirmed=false) to get the analysis brief, then scout_task(confirmed=true, report="<findings>") to save it — do NOT switch agentRole to scout`,
|
|
4758
|
+
'5. Call decompose_task(confirmed=false) — preview the subtask breakdown with file ownership',
|
|
4759
|
+
'6. Fix any file overlap conflicts, then call decompose_task(confirmed=true)',
|
|
4760
|
+
'7. Call get_parallel_kickoffs — it writes builder agent files automatically',
|
|
4761
|
+
'8. Tell the user: open Background Agents panel (⌘⇧J) and start each builder',
|
|
4762
|
+
'9. DO NOT start building. DO NOT claim files. DO NOT edit code.',
|
|
4763
|
+
],
|
|
4764
|
+
blockedUntil: 'decompose_task confirmed=true is called and childTasks are created',
|
|
4765
|
+
}
|
|
4766
|
+
}
|
|
4767
|
+
|
|
4768
|
+
// ── Scout flow ────────────────────────────────────────────────────────
|
|
4769
|
+
if (role === 'scout') {
|
|
4770
|
+
return {
|
|
4771
|
+
flow: 'scout',
|
|
4772
|
+
WARNING: '⛔ SCOUT MODE — READ ONLY. DO NOT write or modify any source file.',
|
|
4773
|
+
mandatorySteps: [
|
|
4774
|
+
'1. READ all files in .cursor/skills/ — log each with log_session_event(type="skill", name=<filename>)',
|
|
4775
|
+
'2. Call recall(taskId) to restore memory from previous sessions',
|
|
4776
|
+
'3. Call scout_task(confirmed=false) to get your analysis brief',
|
|
4777
|
+
'4. Read every relevant source file systematically (routes, models, utils, tests)',
|
|
4778
|
+
'5. Call scout_task(confirmed=true, report="<your findings>") to save the report',
|
|
4779
|
+
'6. DO NOT modify any file. DO NOT create branches. DO NOT commit.',
|
|
4780
|
+
],
|
|
4781
|
+
blockedUntil: 'scout_task confirmed=true with a non-empty report',
|
|
4782
|
+
}
|
|
4783
|
+
}
|
|
4784
|
+
|
|
4785
|
+
// ── Reviewer flow ─────────────────────────────────────────────────────
|
|
4786
|
+
if (role === 'reviewer') {
|
|
4787
|
+
return {
|
|
4788
|
+
flow: 'reviewer',
|
|
4789
|
+
mandatorySteps: [
|
|
4790
|
+
'1. READ all files in .cursor/skills/ — log each with log_session_event(type="skill", name=<filename>)',
|
|
4791
|
+
'2. Call review_pr — read the FULL diff before forming any opinion',
|
|
4792
|
+
'3. Check every changed file line by line against the implementation plan',
|
|
4793
|
+
'4. Call post_pr_review with verdict, analysisPoints (min 2), and specific file+line comments',
|
|
4794
|
+
],
|
|
4795
|
+
blockedUntil: 'post_pr_review called with verdict',
|
|
4796
|
+
}
|
|
4797
|
+
}
|
|
4798
|
+
|
|
4799
|
+
// ── Builder flow (direct / single_builder / decompose child) ──────────
|
|
4800
|
+
const inDecomposeFlow = !!(task.decomposition?.trim())
|
|
4801
|
+
return {
|
|
4802
|
+
flow: inDecomposeFlow ? 'decompose_builder' : flow,
|
|
4803
|
+
SKILLS_GATE: '⛔ DO NOT EDIT ANY FILE until all steps 1–3 are complete.',
|
|
4804
|
+
mandatorySteps: [
|
|
4805
|
+
'1. READ all files in .cursor/skills/ — log each: log_session_event(taskId, type="skill", name=<skill-name>, role="builder")',
|
|
4806
|
+
'2. Call recall(taskId) to restore memory — check for prior session findings',
|
|
4807
|
+
'3. Call get_agent_context(taskId, role="builder") — read implementation plan + claimed files',
|
|
4808
|
+
alreadyHasBranch
|
|
4809
|
+
? `4. You are on branch "${task.github.headBranch}" — run: git pull origin ${task.github.headBranch}`
|
|
4810
|
+
: `4. Call create_branch — creates "${suggestedBranch}" and moves task to in_progress`,
|
|
4811
|
+
'5. Call claim_files with your exact file list — REQUIRED before editing',
|
|
4812
|
+
'6. Implement the feature following the plan. Run tests after each logical unit.',
|
|
4813
|
+
'7. Call commit_helper to stage and commit with a conventional message',
|
|
4814
|
+
'8. Call raise_pr when all subtasks are done and tests pass',
|
|
4815
|
+
],
|
|
4816
|
+
blockedUntil: 'Skills read (step 1), memory recalled (step 2), files claimed (step 5)',
|
|
4817
|
+
inDecomposeFlow,
|
|
4818
|
+
decomposeNote: inDecomposeFlow
|
|
4819
|
+
? '⚡ This is a PARALLEL BUILD. Only edit your claimed files. Do NOT touch files owned by other subtasks.'
|
|
4820
|
+
: null,
|
|
4821
|
+
}
|
|
4822
|
+
})(),
|
|
4225
4823
|
})
|
|
4226
4824
|
}
|
|
4227
4825
|
)
|
|
@@ -4399,18 +4997,40 @@ How to determine status:
|
|
|
4399
4997
|
|
|
4400
4998
|
server.tool(
|
|
4401
4999
|
'submit_task_for_approval',
|
|
4402
|
-
|
|
5000
|
+
`Create and immediately submit an approval request so a human reviewer can approve the plan before any branch or code is created.
|
|
5001
|
+
|
|
5002
|
+
This is a single atomic operation — it creates the approval draft and submits it in one step.
|
|
5003
|
+
Only one approval can be pending at a time. Returns blocked=true if another is already pending.
|
|
5004
|
+
|
|
5005
|
+
IMPORTANT — what to write in "summary":
|
|
5006
|
+
- Write a SHORT, new approval request (3–8 sentences). Do NOT copy the task readmeMarkdown verbatim.
|
|
5007
|
+
- The reviewer already has access to the full plan. Your job is to write a concise summary: what will change, why, and what the key risks are.
|
|
5008
|
+
- Example: "This adds Redis caching to the product list endpoint. It will reduce DB load by ~60% during peak hours. Key risk: cache invalidation on product updates — handled by clearing the cache key on every product.save()."`,
|
|
4403
5009
|
{
|
|
4404
5010
|
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
4405
|
-
title: z.string().describe('Short title for this approval request, e.g. "
|
|
4406
|
-
|
|
4407
|
-
reviewerId: z.string().describe('User ID of the reviewer'),
|
|
5011
|
+
title: z.string().describe('Short title for this approval request, e.g. "Plan: Add Redis caching to product list"'),
|
|
5012
|
+
summary: z.string().describe('A 3–8 sentence approval summary (NOT the full README). Describe: what changes, why, and key risks. The reviewer will read the full plan separately.'),
|
|
5013
|
+
reviewerId: z.string().describe('User ID of the reviewer (admin or project lead)'),
|
|
4408
5014
|
},
|
|
4409
|
-
async ({ taskId, title,
|
|
4410
|
-
// Gate: block if
|
|
5015
|
+
async ({ taskId, title, summary, reviewerId }) => {
|
|
5016
|
+
// Gate: block if another approval is already pending
|
|
4411
5017
|
const taskRes = await api.get(`/api/tasks/${taskId}`)
|
|
4412
5018
|
if (taskRes?.success) {
|
|
4413
|
-
const
|
|
5019
|
+
const task = taskRes.data?.task
|
|
5020
|
+
const alreadyPending = (task?.approvals || []).find(a => a.state === 'pending')
|
|
5021
|
+
if (alreadyPending) {
|
|
5022
|
+
return text({
|
|
5023
|
+
blocked: true,
|
|
5024
|
+
reason: 'An approval is already pending review.',
|
|
5025
|
+
pendingApproval: {
|
|
5026
|
+
title: alreadyPending.title,
|
|
5027
|
+
requestedAt: alreadyPending.requestedAt,
|
|
5028
|
+
},
|
|
5029
|
+
message: 'Wait for the reviewer to decide on the existing request, or ask them to reject it first.',
|
|
5030
|
+
})
|
|
5031
|
+
}
|
|
5032
|
+
// Gate: block if latest test run is failing
|
|
5033
|
+
const latestRun = task?.testRuns?.[0]
|
|
4414
5034
|
if (latestRun && latestRun.status === 'failing') {
|
|
4415
5035
|
return text({
|
|
4416
5036
|
blocked: true,
|
|
@@ -4426,21 +5046,86 @@ How to determine status:
|
|
|
4426
5046
|
})
|
|
4427
5047
|
}
|
|
4428
5048
|
}
|
|
4429
|
-
|
|
5049
|
+
|
|
5050
|
+
// Step 1: create draft
|
|
5051
|
+
const createRes = await api.post(`/api/tasks/${taskId}/approvals`, { title, readme: summary })
|
|
5052
|
+
if (!createRes?.success) return errorText(createRes?.message || 'Could not create approval draft')
|
|
5053
|
+
const freshTask = createRes.data?.task
|
|
5054
|
+
const newApproval = (freshTask?.approvals || []).find(a => a.state === 'none' && a.title === title)
|
|
5055
|
+
if (!newApproval?._id) return errorText('Approval created but could not find its ID — check the task approvals tab')
|
|
5056
|
+
|
|
5057
|
+
// Step 2: immediately submit for review
|
|
5058
|
+
const submitRes = await api.post(`/api/tasks/${taskId}/approvals/${newApproval._id}/submit`, { reviewerId })
|
|
5059
|
+
if (!submitRes?.success) {
|
|
5060
|
+
return text({
|
|
5061
|
+
draftCreated: true,
|
|
5062
|
+
approvalId: newApproval._id,
|
|
5063
|
+
submitFailed: true,
|
|
5064
|
+
reason: submitRes?.message || 'Draft created but submit step failed',
|
|
5065
|
+
message: 'The approval draft was created but could not be submitted automatically. Open the task → Approval tab → click "Submit for review" manually.',
|
|
5066
|
+
})
|
|
5067
|
+
}
|
|
5068
|
+
return text({
|
|
5069
|
+
submitted: true,
|
|
5070
|
+
approvalId: newApproval._id,
|
|
5071
|
+
title,
|
|
5072
|
+
state: 'pending',
|
|
5073
|
+
message: `Approval request submitted. The reviewer will be notified. Do NOT start coding or create a branch until they approve.`,
|
|
5074
|
+
nextStep: `Wait for decide_task_approval to return decision="approve". Then call create_branch.`,
|
|
5075
|
+
})
|
|
4430
5076
|
}
|
|
4431
5077
|
)
|
|
4432
5078
|
|
|
4433
5079
|
server.tool(
|
|
4434
5080
|
'decide_task_approval',
|
|
4435
|
-
|
|
5081
|
+
`Approve or reject a specific approval request by approvalId. Only the designated reviewer can call this.
|
|
5082
|
+
|
|
5083
|
+
BEFORE calling with a decision, review the plan:
|
|
5084
|
+
- Read the plan summary returned by this tool (confirmed=false)
|
|
5085
|
+
- Read the task comments for any context from the submitter
|
|
5086
|
+
- Check the implementation plan for completeness, correctness, and feasibility
|
|
5087
|
+
- Approve if the plan is clear and actionable; reject with a specific note explaining what needs to change
|
|
5088
|
+
|
|
5089
|
+
Set confirmed=false first to read the plan, then call again with confirmed=true and your decision.`,
|
|
4436
5090
|
{
|
|
4437
5091
|
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
4438
5092
|
approvalId: z.string().describe("Approval request's MongoDB ObjectId (from the task's approvals array)"),
|
|
4439
|
-
decision: z.enum(['approve', 'reject']),
|
|
4440
|
-
note: z.string().optional().describe('Reason for the decision'),
|
|
5093
|
+
decision: z.enum(['approve', 'reject']).optional().describe('Your decision — omit to just preview the plan'),
|
|
5094
|
+
note: z.string().optional().describe('Reason for the decision (required on reject, recommended on approve)'),
|
|
5095
|
+
confirmed: z.boolean().optional().default(false).describe('Set true to submit the decision after reviewing the plan'),
|
|
4441
5096
|
},
|
|
4442
|
-
async ({ taskId, approvalId, decision, note }) =>
|
|
4443
|
-
|
|
5097
|
+
async ({ taskId, approvalId, decision, note, confirmed = false }) => {
|
|
5098
|
+
// Always fetch task so reviewer can read the plan before deciding
|
|
5099
|
+
const taskRes = await api.get(`/api/tasks/${taskId}`)
|
|
5100
|
+
const task = taskRes?.data?.task
|
|
5101
|
+
if (!task) return errorText('Task not found')
|
|
5102
|
+
|
|
5103
|
+
const approval = (task.approvals || []).find(a => String(a._id) === String(approvalId))
|
|
5104
|
+
if (!approval) return errorText('Approval request not found on this task')
|
|
5105
|
+
|
|
5106
|
+
if (!confirmed || !decision) {
|
|
5107
|
+
// Preview mode — return plan so reviewer can read it first
|
|
5108
|
+
const planLines = (approval.readme || '').split('\n')
|
|
5109
|
+
const planPreview = planLines.slice(0, 80).join('\n') + (planLines.length > 80 ? '\n\n[…plan truncated, first 80 lines shown]' : '')
|
|
5110
|
+
return text({
|
|
5111
|
+
task: { key: task.key, title: task.title, priority: task.priority },
|
|
5112
|
+
approval: { id: approval._id, title: approval.title, state: approval.state,
|
|
5113
|
+
submittedBy: approval.requestedBy?.name || approval.requestedBy?.email || 'unknown',
|
|
5114
|
+
submittedAt: approval.requestedAt },
|
|
5115
|
+
planPreview,
|
|
5116
|
+
reviewChecklist: [
|
|
5117
|
+
'Is the plan clear and specific? (not vague high-level bullets)',
|
|
5118
|
+
'Does it identify the correct files/modules to change?',
|
|
5119
|
+
'Does it handle edge cases and error paths?',
|
|
5120
|
+
'Is the scope reasonable for a single task?',
|
|
5121
|
+
'Any security, data-loss, or regression risks?',
|
|
5122
|
+
],
|
|
5123
|
+
message: `Review the plan above, then call decide_task_approval again with confirmed=true and decision="approve" or "reject".`,
|
|
5124
|
+
})
|
|
5125
|
+
}
|
|
5126
|
+
|
|
5127
|
+
return call(() => api.post(`/api/tasks/${taskId}/approvals/${approvalId}/decide`, { decision, note }))
|
|
5128
|
+
}
|
|
4444
5129
|
)
|
|
4445
5130
|
}
|
|
4446
5131
|
|
|
@@ -5129,8 +5814,72 @@ function writeCursorWorkspace(task, projectAgentConfig, startPath) {
|
|
|
5129
5814
|
mkdirSync(commandsDir, { recursive: true })
|
|
5130
5815
|
mkdirSync(rulesDir, { recursive: true })
|
|
5131
5816
|
|
|
5817
|
+
// ── Ensure generated AI files are NOT committed by the developer ────────────
|
|
5818
|
+
// These are runtime files — they change per task and per agent session.
|
|
5819
|
+
// They belong in the developer's local env, not in git history.
|
|
5820
|
+
const gitignorePath = join(repoRoot, '.gitignore')
|
|
5821
|
+
const AI_GITIGNORE_ENTRIES = [
|
|
5822
|
+
'# InternalTool AI workspace — auto-generated, do not commit',
|
|
5823
|
+
'.cursor/agents/',
|
|
5824
|
+
'.cursor/skills/',
|
|
5825
|
+
'.cursor/commands/',
|
|
5826
|
+
'.cursor/rules/',
|
|
5827
|
+
'.claude/',
|
|
5828
|
+
'.internaltool-active-task',
|
|
5829
|
+
]
|
|
5830
|
+
try {
|
|
5831
|
+
let existing = ''
|
|
5832
|
+
if (existsSync(gitignorePath)) existing = readFileSync(gitignorePath, 'utf8')
|
|
5833
|
+
const missing = AI_GITIGNORE_ENTRIES.filter(e => e.startsWith('#') ? false : !existing.includes(e))
|
|
5834
|
+
if (missing.length > 0) {
|
|
5835
|
+
const toAppend = '\n' + AI_GITIGNORE_ENTRIES[0] + '\n' + missing.join('\n') + '\n'
|
|
5836
|
+
writeFileSync(gitignorePath, existing + toAppend, 'utf8')
|
|
5837
|
+
}
|
|
5838
|
+
} catch { /* non-fatal — gitignore update is best-effort */ }
|
|
5839
|
+
|
|
5132
5840
|
const written = []
|
|
5133
5841
|
|
|
5842
|
+
// ── 0. Session protocol rule — always-apply, enforces skill gate ──────────
|
|
5843
|
+
// This is the first rule Cursor sees (filename 00- sorts before all others).
|
|
5844
|
+
// It forces the agent to read skills before editing — the root cause of
|
|
5845
|
+
// builders skipping the skill step was the lack of a hard runtime constraint.
|
|
5846
|
+
const sessionProtocolRule = `---
|
|
5847
|
+
description: InternalTool session protocol — enforced for every agent session on this task.
|
|
5848
|
+
alwaysApply: true
|
|
5849
|
+
---
|
|
5850
|
+
|
|
5851
|
+
# ⛔ MANDATORY SESSION PROTOCOL
|
|
5852
|
+
|
|
5853
|
+
**You MUST follow these steps IN ORDER before editing any file.**
|
|
5854
|
+
Skipping any step is a protocol violation and will result in incorrect implementation.
|
|
5855
|
+
|
|
5856
|
+
## Step 1 — Read all skills (REQUIRED before any Edit/Write)
|
|
5857
|
+
For each file in \`.cursor/skills/\`:
|
|
5858
|
+
1. Read the file completely
|
|
5859
|
+
2. Call \`log_session_event\` with type="skill", name=<filename without .md>, role=<your role>
|
|
5860
|
+
|
|
5861
|
+
Do NOT call Edit or Write until every skill file has been read and logged.
|
|
5862
|
+
|
|
5863
|
+
## Step 2 — Recall memory
|
|
5864
|
+
Call \`recall(taskId="${taskId}")\` to restore facts from previous sessions.
|
|
5865
|
+
If memory contains a \`status\` key, read it — the task may have known blockers.
|
|
5866
|
+
|
|
5867
|
+
## Step 3 — Get full context
|
|
5868
|
+
Call \`get_agent_context(taskId="${taskId}", role=<your role>)\`.
|
|
5869
|
+
Read the implementation plan, claimed files, and scout report before proceeding.
|
|
5870
|
+
|
|
5871
|
+
## Step 4 — Claim files (builders only)
|
|
5872
|
+
Call \`claim_files\` with every file you will edit BEFORE your first Edit call.
|
|
5873
|
+
Editing unclaimed files causes merge conflicts with other agents.
|
|
5874
|
+
|
|
5875
|
+
---
|
|
5876
|
+
|
|
5877
|
+
**Role-specific hard constraints are in \`.cursor/agents/active-agent.md\`.**
|
|
5878
|
+
**Project-level rules are in \`.cursor/rules/\` (other .mdc files).**
|
|
5879
|
+
`
|
|
5880
|
+
writeFileSync(join(rulesDir, '00-session-protocol.mdc'), sessionProtocolRule, 'utf8')
|
|
5881
|
+
written.push(join(rulesDir, '00-session-protocol.mdc'))
|
|
5882
|
+
|
|
5134
5883
|
// ── 1. Project-level rules from DB → .cursor/rules/<name>.mdc ─────────────
|
|
5135
5884
|
const projectRules = cfg.rules || []
|
|
5136
5885
|
for (const r of projectRules) {
|
|
@@ -5394,23 +6143,41 @@ log_session_event(type="info", name="conflict-resolved", summary="<what you merg
|
|
|
5394
6143
|
|
|
5395
6144
|
/**
|
|
5396
6145
|
* Delete task-specific workspace files written at kickoff.
|
|
5397
|
-
*
|
|
5398
|
-
*
|
|
6146
|
+
* Removes: active-agent.md, start-task.md command, and ALL skill files in .cursor/skills/.
|
|
6147
|
+
* Project-level rules (.cursor/rules/*.mdc) are kept — they are curated by the admin, not generated per task.
|
|
5399
6148
|
*/
|
|
5400
6149
|
function deleteCursorWorkspace(role, startPath) {
|
|
5401
6150
|
const deleted = []
|
|
5402
6151
|
try {
|
|
5403
6152
|
const repoRoot = findRepoRoot(startPath)
|
|
5404
6153
|
if (!repoRoot) return deleted
|
|
5405
|
-
|
|
6154
|
+
|
|
6155
|
+
// Always-delete: task-scoped agent and command files
|
|
6156
|
+
const alwaysDelete = [
|
|
5406
6157
|
join(repoRoot, '.cursor', 'agents', 'active-agent.md'),
|
|
5407
6158
|
join(repoRoot, '.cursor', 'commands', 'start-task.md'),
|
|
5408
6159
|
]
|
|
5409
|
-
for (const f of
|
|
6160
|
+
for (const f of alwaysDelete) {
|
|
5410
6161
|
try {
|
|
5411
6162
|
if (existsSync(f)) { unlinkSync(f); deleted.push(f) }
|
|
5412
6163
|
} catch { /* non-fatal */ }
|
|
5413
6164
|
}
|
|
6165
|
+
|
|
6166
|
+
// Delete all skill files — they are fetched fresh on every kickoff
|
|
6167
|
+
// based on role and task type. Keeping stale skills causes context bloat.
|
|
6168
|
+
const skillsDir = join(repoRoot, '.cursor', 'skills')
|
|
6169
|
+
if (existsSync(skillsDir)) {
|
|
6170
|
+
try {
|
|
6171
|
+
const skillFiles = readdirSync(skillsDir).filter(f => f.endsWith('.md'))
|
|
6172
|
+
for (const f of skillFiles) {
|
|
6173
|
+
try {
|
|
6174
|
+
const fullPath = join(skillsDir, f)
|
|
6175
|
+
unlinkSync(fullPath)
|
|
6176
|
+
deleted.push(fullPath)
|
|
6177
|
+
} catch { /* non-fatal */ }
|
|
6178
|
+
}
|
|
6179
|
+
} catch { /* non-fatal */ }
|
|
6180
|
+
}
|
|
5414
6181
|
} catch { /* non-fatal */ }
|
|
5415
6182
|
return deleted
|
|
5416
6183
|
}
|
|
@@ -5521,7 +6288,17 @@ async function apiWithRetry(fn, maxRetries = 2, initialDelay = 500) {
|
|
|
5521
6288
|
}
|
|
5522
6289
|
|
|
5523
6290
|
function wrapApiError(e) {
|
|
5524
|
-
|
|
6291
|
+
const cause = e._cause || 'unknown'
|
|
6292
|
+
const isAuth = cause === 'auth'
|
|
6293
|
+
return {
|
|
6294
|
+
error: true,
|
|
6295
|
+
cause,
|
|
6296
|
+
message: e.message?.split('\n')[0],
|
|
6297
|
+
...(isAuth ? {
|
|
6298
|
+
authError: true,
|
|
6299
|
+
fix: 'Your session token has expired or is invalid. Ask the developer to restart the MCP server (which re-reads the INTERNALTOOL_API_KEY env var) or re-run: npx internaltool-mcp to reconnect.',
|
|
6300
|
+
} : {}),
|
|
6301
|
+
}
|
|
5525
6302
|
}
|
|
5526
6303
|
|
|
5527
6304
|
// ── #9 Get last git commit metadata ──────────────────────────────────────────
|
|
@@ -6065,31 +6842,39 @@ If you have uncommitted tracked changes, it will tell you exactly what to do bef
|
|
|
6065
6842
|
const task = taskRes.data.task
|
|
6066
6843
|
|
|
6067
6844
|
// ── Approval gate check ───────────────────────────────────────────────────
|
|
6068
|
-
//
|
|
6069
|
-
// Detect this early and guide the developer instead of failing silently after branch creation.
|
|
6845
|
+
// Block branch creation (at both confirmed=false AND confirmed=true) until the plan is approved.
|
|
6070
6846
|
const pendingApv2 = (task.approvals || []).find(a => a.state === 'pending') || null
|
|
6071
6847
|
const hasApprovedApv2 = (task.approvals || []).some(a => a.state === 'approved')
|
|
6072
6848
|
const approvalState = pendingApv2 ? 'pending' : hasApprovedApv2 ? 'approved' : 'none'
|
|
6073
6849
|
const PLANNING_COLS = ['backlog', 'todo']
|
|
6074
6850
|
const needsApproval = PLANNING_COLS.includes(task.column) && !hasApprovedApv2
|
|
6075
|
-
if (needsApproval
|
|
6851
|
+
if (needsApproval) {
|
|
6076
6852
|
const isFix2 = /\b(fix|bug|hotfix|patch)\b/i.test(task.title + ' ' + (task.description || ''))
|
|
6077
6853
|
const slug2 = task.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 35)
|
|
6078
|
-
|
|
6854
|
+
// Fetch devSlug here too so the preview matches the actual branch name
|
|
6855
|
+
let devSlug2 = ''
|
|
6856
|
+
try {
|
|
6857
|
+
const meRes2 = await api.get('/api/auth/me')
|
|
6858
|
+
const devName2 = meRes2?.data?.user?.name || meRes2?.data?.name || ''
|
|
6859
|
+
devSlug2 = devName2.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 15)
|
|
6860
|
+
} catch { /* non-fatal */ }
|
|
6861
|
+
const previewBranch = devSlug2
|
|
6862
|
+
? `${devSlug2}/${isFix2 ? 'fix' : 'feature'}/${task.key.toLowerCase()}-${slug2}`
|
|
6863
|
+
: `${isFix2 ? 'fix' : 'feature'}/${task.key.toLowerCase()}-${slug2}`
|
|
6079
6864
|
return text({
|
|
6080
6865
|
blocked: true,
|
|
6081
6866
|
reason: 'approval_required',
|
|
6082
6867
|
task: { key: task.key, title: task.title, column: task.column, approvalState },
|
|
6083
|
-
message:
|
|
6868
|
+
message: `⛔ The plan must be approved before creating a branch. Do NOT code until the reviewer approves.`,
|
|
6084
6869
|
approvalStatus: approvalState === 'pending'
|
|
6085
|
-
? `Plan is
|
|
6870
|
+
? `Plan is submitted and waiting for reviewer approval. Once approved, call create_branch again.`
|
|
6086
6871
|
: `Plan has not been submitted for review yet.`,
|
|
6087
6872
|
nextSteps: approvalState === 'pending'
|
|
6088
|
-
? [`Wait for
|
|
6873
|
+
? [`Wait for decide_task_approval to return decision="approve", then call create_branch with taskId="${taskId}"`]
|
|
6089
6874
|
: [
|
|
6090
|
-
`1.
|
|
6091
|
-
`2. Call submit_task_for_approval with taskId="${taskId}"
|
|
6092
|
-
`3. Once approved
|
|
6875
|
+
`1. Write the implementation plan: call update_task with readmeMarkdown if not already written`,
|
|
6876
|
+
`2. Call submit_task_for_approval with taskId="${taskId}", summary="<3-8 sentence summary>", reviewerId="<admin user id>"`,
|
|
6877
|
+
`3. Once approved: call create_branch — it will create "${previewBranch}" and move the task to In progress`,
|
|
6093
6878
|
],
|
|
6094
6879
|
})
|
|
6095
6880
|
}
|
|
@@ -6481,7 +7266,7 @@ Set confirmed=false first to preview the full plan, then confirmed=true to save
|
|
|
6481
7266
|
})
|
|
6482
7267
|
}
|
|
6483
7268
|
|
|
6484
|
-
// Park Task B if provided
|
|
7269
|
+
// Park Task B if provided — and clean up its workspace so skills don't bleed into Task A
|
|
6485
7270
|
if (taskB && taskBId) {
|
|
6486
7271
|
try {
|
|
6487
7272
|
await api.patch(`/api/tasks/${taskBId}/park`, {
|
|
@@ -6490,6 +7275,14 @@ Set confirmed=false first to preview the full plan, then confirmed=true to save
|
|
|
6490
7275
|
blockers: '',
|
|
6491
7276
|
})
|
|
6492
7277
|
} catch { /* non-fatal — developer can park manually */ }
|
|
7278
|
+
// Delete Task B's cursor workspace — same cleanup that park_task does.
|
|
7279
|
+
// Without this, Task B's skills and session-protocol stay on disk while
|
|
7280
|
+
// working on Task A's branch, and the agent may read the wrong context.
|
|
7281
|
+
try { deleteCursorWorkspace(taskB?.agentRole || null, process.cwd()) } catch { /* non-fatal */ }
|
|
7282
|
+
try { unlinkSync(join(process.cwd(), '.internaltool-active-task')) } catch { /* non-fatal */ }
|
|
7283
|
+
api.patch(`/api/tasks/${taskBId}`, {
|
|
7284
|
+
agentWorkspace: { clearedAt: new Date().toISOString() }
|
|
7285
|
+
}).catch(() => {})
|
|
6493
7286
|
}
|
|
6494
7287
|
|
|
6495
7288
|
return text({
|
|
@@ -6661,21 +7454,33 @@ Set confirmed=false first to preview the full PR content, then confirmed=true to
|
|
|
6661
7454
|
})
|
|
6662
7455
|
if (!res?.success) return errorText(res?.message || 'Could not create PR')
|
|
6663
7456
|
|
|
6664
|
-
// Delete the
|
|
7457
|
+
// Delete the full cursor workspace (skills, rules, active-agent) — coding is done
|
|
6665
7458
|
const deletedRulesFile = deleteCursorRulesFile(task.key, repoPath)
|
|
7459
|
+
deleteCursorWorkspace(task?.agentRole || null, repoPath || process.cwd())
|
|
7460
|
+
|
|
7461
|
+
// Move task to in_review directly — don't rely solely on GitHub webhook
|
|
7462
|
+
// (webhook may not be configured, or may be delayed)
|
|
7463
|
+
try {
|
|
7464
|
+
await api.post(`/api/tasks/${taskId}/move`, { column: 'in_review', toIndex: 0 })
|
|
7465
|
+
} catch { /* non-fatal — webhook will do it if direct patch fails */ }
|
|
7466
|
+
|
|
7467
|
+
// Remove .internaltool-active-task marker
|
|
7468
|
+
try { unlinkSync(join(process.cwd(), '.internaltool-active-task')) } catch { /* non-fatal */ }
|
|
6666
7469
|
|
|
6667
7470
|
return text({
|
|
6668
7471
|
prNumber: res.data.prNumber,
|
|
6669
7472
|
prUrl: res.data.prUrl,
|
|
6670
7473
|
title: prTitle,
|
|
6671
7474
|
draft,
|
|
6672
|
-
message: `PR #${res.data.prNumber} created.`,
|
|
6673
|
-
|
|
6674
|
-
|
|
6675
|
-
:
|
|
7475
|
+
message: `PR #${res.data.prNumber} created. Task moved to In Review.`,
|
|
7476
|
+
cursorWorkspaceCleared: {
|
|
7477
|
+
cleared: true,
|
|
7478
|
+
rulesFile: deletedRulesFile || null,
|
|
7479
|
+
note: 'Cursor workspace (skills + rules + active-agent) deleted — coding session is complete.',
|
|
7480
|
+
},
|
|
6676
7481
|
nextStep: draft
|
|
6677
7482
|
? 'PR is a draft. Mark it ready for review on GitHub when you want reviewer notifications to fire.'
|
|
6678
|
-
:
|
|
7483
|
+
: `PR is live at ${res.data.prUrl}. Task is now in_review — the reviewer can call kickoff_task with agentRole="reviewer".`,
|
|
6679
7484
|
})
|
|
6680
7485
|
} catch (e) {
|
|
6681
7486
|
return errorText(e.message)
|
|
@@ -7410,12 +8215,13 @@ Use this when a developer asks: "what do I do?", "how do I rebase?", "I have con
|
|
|
7410
8215
|
} else {
|
|
7411
8216
|
steps.push({
|
|
7412
8217
|
step: stepNum++,
|
|
7413
|
-
title: `
|
|
7414
|
-
why: `Your PR is already open. After pushing the rebase,
|
|
8218
|
+
title: `Get your PR reviewed — PR #${branch.prNumber}`,
|
|
8219
|
+
why: `Your PR is already open. After pushing the rebase, ask your reviewer to approve so it can be merged.`,
|
|
7415
8220
|
commands: [
|
|
7416
|
-
{ label: '
|
|
8221
|
+
{ label: 'Open the PR in your browser to add reviewers', cmd: `gh pr view --web ${branch.prNumber}` },
|
|
8222
|
+
{ label: 'Or check PR status in the terminal', cmd: `gh pr status` },
|
|
7417
8223
|
],
|
|
7418
|
-
note: `PR URL: ${branch.prUrl || 'see GitHub'}
|
|
8224
|
+
note: `PR URL: ${branch.prUrl || 'see GitHub'}. Add reviewers on GitHub (top-right "Reviewers" panel) — do NOT use "gh pr review --request-changes" (that submits a rejection verdict, not a review request).`,
|
|
7419
8225
|
})
|
|
7420
8226
|
}
|
|
7421
8227
|
|
|
@@ -7818,7 +8624,7 @@ Use this to remember decisions, constraints, context, and findings that should p
|
|
|
7818
8624
|
Call recall(taskId) to retrieve everything at any time.`,
|
|
7819
8625
|
{
|
|
7820
8626
|
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
7821
|
-
key: z.string().describe('Memory key — short, descriptive, no spaces (e.g. "arch_decision", "constraint_auth")'),
|
|
8627
|
+
key: z.string().describe('Memory key — short, descriptive, no spaces (e.g. "arch_decision", "constraint_auth"). In parallel builds with multiple agents on the same task, prefix with your role/index to avoid collisions (e.g. "builder1_discovered", "builder2_constraint"). Last write to the same key wins.'),
|
|
7822
8628
|
value: z.string().describe('Value to store — plain text, any length'),
|
|
7823
8629
|
},
|
|
7824
8630
|
async ({ taskId, key, value }) => {
|
|
@@ -7897,8 +8703,8 @@ Always call this first — it collapses context from the last 3 steps into one c
|
|
|
7897
8703
|
if (repoPath && task.github?.headBranch) {
|
|
7898
8704
|
try {
|
|
7899
8705
|
const branch = task.github.headBranch
|
|
7900
|
-
const status
|
|
7901
|
-
const logLine =
|
|
8706
|
+
const status = runGit('status --short', repoPath)
|
|
8707
|
+
const logLine = runGit('log --oneline -1', repoPath)
|
|
7902
8708
|
localState = { branch, dirty: status.length > 0, lastCommit: logLine }
|
|
7903
8709
|
} catch { localState = { error: 'Could not read local git state' } }
|
|
7904
8710
|
}
|