internaltool-mcp 1.6.41 → 1.6.45
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 +532 -126
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -463,76 +463,126 @@ Returns tasks with key, title, column, assignees, priority, taskType, and branch
|
|
|
463
463
|
}
|
|
464
464
|
)
|
|
465
465
|
|
|
466
|
-
// ──
|
|
466
|
+
// ── suggest_skill ─────────────────────────────────────────────────────────────
|
|
467
467
|
server.tool(
|
|
468
|
-
'
|
|
469
|
-
`
|
|
468
|
+
'suggest_skill',
|
|
469
|
+
`Propose a reusable skill, rule, prompt, subagent, or hook to the project's AgentKit — pending human approval.
|
|
470
470
|
|
|
471
|
-
|
|
472
|
-
contain a real implementation plan based on the actual codebase, not a generic description.
|
|
471
|
+
## When to call this
|
|
473
472
|
|
|
474
|
-
|
|
473
|
+
Call this when you discover something during a coding session that would help future sessions in this codebase:
|
|
475
474
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
475
|
+
- **skill**: A step-by-step methodology you used (e.g. "how to add a new API endpoint in this Express app", "how to run and interpret the test suite")
|
|
476
|
+
- **rule**: A constraint that should always apply (e.g. "always validate with zod before touching the DB", "never modify migrations directly")
|
|
477
|
+
- **prompt**: A prompt template that reliably produces good results for this project (e.g. "security sweep prompt", "PR review checklist")
|
|
478
|
+
- **subagent**: A specialized agent persona (e.g. "database migration specialist with access to prisma tools only")
|
|
479
|
+
- **hook**: A shell command that should run automatically (e.g. "check .internaltool-active-task before any Edit", "run lint after Write")
|
|
479
480
|
|
|
480
|
-
|
|
481
|
-
Using your native Read / Grep / Glob tools:
|
|
481
|
+
## When NOT to call this
|
|
482
482
|
|
|
483
|
-
|
|
484
|
-
|
|
483
|
+
- Do not suggest things that are obvious from the README or existing docs
|
|
484
|
+
- Do not suggest generic skills that apply to any codebase (e.g. "how to write clean code")
|
|
485
|
+
- Do not suggest more than 1-2 skills per session — be selective and high-signal
|
|
485
486
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
487
|
+
## The human reviews and decides
|
|
488
|
+
|
|
489
|
+
Your suggestion goes into a "Pending" queue visible in the project's AgentKit UI.
|
|
490
|
+
The developer can edit the body, approve (which promotes it to active config), or reject.
|
|
491
|
+
After approval, the skill/rule is injected at every future kickoff_task.`,
|
|
492
|
+
{
|
|
493
|
+
projectId: z.string().describe("Project's MongoDB ObjectId"),
|
|
494
|
+
type: z.enum(['skill', 'rule', 'prompt', 'subagent', 'hook']).describe('What kind of suggestion this is'),
|
|
495
|
+
name: z.string().describe('Slug name — lowercase, hyphens, no spaces (e.g. "add-express-route", "require-zod-validation")'),
|
|
496
|
+
description: z.string().describe('One-line description of what this does — shown in the AgentKit UI'),
|
|
497
|
+
body: z.string().describe('Full markdown content — for skills: step-by-step instructions; for rules: constraint language; for hooks: shell command + explanation'),
|
|
498
|
+
rationale: z.string().describe('Why this would help future sessions — what problem it solves, what mistake it prevents, or what pattern it captures'),
|
|
499
|
+
sourceTaskId: z.string().optional().describe('Task ID this was discovered during (for traceability)'),
|
|
500
|
+
sourceTaskKey: z.string().optional().describe('Task key (e.g. PROJ-42) for display'),
|
|
501
|
+
// Hook-specific
|
|
502
|
+
hookTrigger: z.string().optional().describe('For type=hook: when it fires (PreToolUse | PostToolUse | Stop)'),
|
|
503
|
+
hookMatcher: z.string().optional().describe('For type=hook: tool name regex matcher (e.g. "Edit|Write")'),
|
|
504
|
+
hookCommand: z.string().optional().describe('For type=hook: the shell command to execute'),
|
|
505
|
+
},
|
|
506
|
+
async ({ projectId, type, name, description, body, rationale, sourceTaskId, sourceTaskKey, hookTrigger, hookMatcher, hookCommand }) => {
|
|
507
|
+
try { assertProjectScope(projectId) } catch (e) { return errorText(e.message) }
|
|
508
|
+
return call(() => api.post(`/api/projects/${projectId}/skill-suggestions`, {
|
|
509
|
+
type, name, description, body, rationale,
|
|
510
|
+
sourceTaskId: sourceTaskId || null,
|
|
511
|
+
sourceTaskKey: sourceTaskKey || '',
|
|
512
|
+
suggestedBy: 'Claude Code',
|
|
513
|
+
hookTrigger: hookTrigger || '',
|
|
514
|
+
hookMatcher: hookMatcher || '',
|
|
515
|
+
hookCommand: hookCommand || '',
|
|
516
|
+
}))
|
|
517
|
+
}
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
// ── plan_task_from_codebase ───────────────────────────────────────────────────
|
|
521
|
+
server.tool(
|
|
522
|
+
'plan_task_from_codebase',
|
|
523
|
+
`Specialized task-creation agent — scouts the codebase, sets up the agent config, and creates a fully-planned, kickoff-ready task.
|
|
490
524
|
|
|
491
|
-
|
|
492
|
-
Note: naming conventions, folder structure, how services/routes/components are wired.
|
|
525
|
+
## MANDATORY protocol — follow every step in order
|
|
493
526
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
527
|
+
### Step 1 — Duplicate check
|
|
528
|
+
Call search_tasks(projectId, query="<keywords from the request>").
|
|
529
|
+
If a similar open task exists → return it with kickoff instructions. Do NOT create a duplicate.
|
|
497
530
|
|
|
498
|
-
|
|
531
|
+
### Step 2 — Codebase scout (READ the code — do not guess)
|
|
532
|
+
Using your native Read / Grep / Glob tools:
|
|
499
533
|
|
|
500
|
-
|
|
501
|
-
|
|
534
|
+
a) **Stack detection** — read package.json / go.mod / requirements.txt / Cargo.toml.
|
|
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:
|
|
502
559
|
- ## Goal — one sentence
|
|
503
|
-
- ## Stack —
|
|
504
|
-
- ## Technical approach —
|
|
505
|
-
- ## Files to create
|
|
506
|
-
- ## Files to modify
|
|
507
|
-
- ## Subtasks — ordered
|
|
508
|
-
- ## Acceptance criteria
|
|
509
|
-
|
|
510
|
-
### Step 4 — Determine task metadata
|
|
511
|
-
- taskType: feature / bugfix / migration / integration / ui / backend / security / refactor
|
|
512
|
-
- priority: low / medium / high / critical (use "high" if the request sounds important)
|
|
513
|
-
- 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
|
|
514
566
|
|
|
515
567
|
### Step 5 — Create the task
|
|
516
568
|
Call create_task with:
|
|
517
|
-
- projectId (
|
|
518
|
-
-
|
|
519
|
-
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
-
|
|
530
|
-
-
|
|
531
|
-
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
Do NOT skip the codebase analysis. Do NOT create the task before reading the code.
|
|
535
|
-
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.`,
|
|
536
586
|
{
|
|
537
587
|
projectId: z.string().describe("InternalTool project's MongoDB ObjectId — from the project's task board URL or CLAUDE.md"),
|
|
538
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")'),
|
|
@@ -550,8 +600,9 @@ Do NOT ask the developer to describe the codebase — read it yourself.`,
|
|
|
550
600
|
const dirTree = getDirTree(cwd, 2)
|
|
551
601
|
const entryPoints = findEntryPoints(cwd, stack)
|
|
552
602
|
|
|
553
|
-
// ── 2. Fetch project context
|
|
603
|
+
// ── 2. Fetch project context + existing agentConfig ─────────────────────
|
|
554
604
|
let projectContext = null
|
|
605
|
+
let existingAgentConfig = { skills: [], rules: [], subagents: [], prompts: [] }
|
|
555
606
|
try {
|
|
556
607
|
const projRes = await api.get(`/api/projects/${projectId}`)
|
|
557
608
|
if (projRes?.success) {
|
|
@@ -561,6 +612,13 @@ Do NOT ask the developer to describe the codebase — read it yourself.`,
|
|
|
561
612
|
taskCount: (projRes.data.tasks || []).length,
|
|
562
613
|
githubRepo: p.github?.repoUrl || null,
|
|
563
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
|
+
}
|
|
564
622
|
}
|
|
565
623
|
} catch { /* non-fatal */ }
|
|
566
624
|
|
|
@@ -578,7 +636,102 @@ Do NOT ask the developer to describe the codebase — read it yourself.`,
|
|
|
578
636
|
}
|
|
579
637
|
} catch { /* non-fatal */ }
|
|
580
638
|
|
|
581
|
-
// ── 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 ──────────────────
|
|
582
735
|
const hasEntryPoints = Object.keys(entryPoints).length > 0
|
|
583
736
|
const stackSummary = [
|
|
584
737
|
stack.language, stack.framework, stack.testRunner, ...(stack.extra || [])
|
|
@@ -595,6 +748,26 @@ Do NOT ask the developer to describe the codebase — read it yourself.`,
|
|
|
595
748
|
: { note: 'No entry points found at cwd — check that MCP server is running from the project root' },
|
|
596
749
|
},
|
|
597
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
|
+
|
|
598
771
|
// Duplicate guard
|
|
599
772
|
...(similarTasks?.length > 0 && {
|
|
600
773
|
duplicateWarning: true,
|
|
@@ -602,16 +775,21 @@ Do NOT ask the developer to describe the codebase — read it yourself.`,
|
|
|
602
775
|
duplicateInstruction: 'Check the list above. If one matches → call kickoff_task on it. If none match → proceed.',
|
|
603
776
|
}),
|
|
604
777
|
|
|
605
|
-
// What to do now
|
|
778
|
+
// What to do now
|
|
606
779
|
nextSteps: [
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
'
|
|
612
|
-
|
|
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),
|
|
613
792
|
|
|
614
|
-
// Context
|
|
615
793
|
request,
|
|
616
794
|
projectId,
|
|
617
795
|
priority,
|
|
@@ -661,8 +839,13 @@ Always prefer column="todo" so the task is visibly ready to start.`,
|
|
|
661
839
|
.describe('Ordered implementation checklist — shown in the task UI and read by agents at kickoff. Order matters: schema → model → service → route → test → frontend.'),
|
|
662
840
|
suggestedFiles: z.array(z.string()).optional()
|
|
663
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.'),
|
|
664
847
|
},
|
|
665
|
-
async ({ projectId, suggestedFiles, ...taskData }) => {
|
|
848
|
+
async ({ projectId, suggestedFiles, agentKitOverrides, ...taskData }) => {
|
|
666
849
|
try { assertProjectScope(projectId) } catch (e) { return errorText(e.message) }
|
|
667
850
|
|
|
668
851
|
// Append suggested files section to the README so the builder sees them at kickoff
|
|
@@ -682,18 +865,37 @@ Always prefer column="todo" so the task is visibly ready to start.`,
|
|
|
682
865
|
if (!res?.success) return errorText(res?.message || 'Failed to create task')
|
|
683
866
|
|
|
684
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
|
+
|
|
685
880
|
return text({
|
|
686
|
-
created:
|
|
687
|
-
taskId:
|
|
688
|
-
taskKey:
|
|
689
|
-
title:
|
|
690
|
-
column:
|
|
691
|
-
taskType:
|
|
692
|
-
subtasks:
|
|
693
|
-
|
|
694
|
-
|
|
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')
|
|
695
897
|
: null,
|
|
696
|
-
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.`,
|
|
697
899
|
})
|
|
698
900
|
}
|
|
699
901
|
)
|
|
@@ -1858,16 +2060,45 @@ Returns systemPrompt ready to use as a Claude system prompt.`,
|
|
|
1858
2060
|
} catch { /* non-fatal */ }
|
|
1859
2061
|
}
|
|
1860
2062
|
|
|
1861
|
-
//
|
|
1862
|
-
//
|
|
2063
|
+
// Filter skills to only those relevant to this role and task type.
|
|
2064
|
+
// An agent should NOT load every project skill — only what it needs for this specific task.
|
|
1863
2065
|
const allProjectSkills = ctx.project?.agentConfig?.skills || []
|
|
1864
2066
|
const taskSkillOverrides = ctx.task?.agentKitOverrides?.skills || {}
|
|
1865
|
-
const
|
|
2067
|
+
const enabledSkills = allProjectSkills.filter(s => {
|
|
1866
2068
|
const taskOverride = taskSkillOverrides[s.name]
|
|
1867
2069
|
return taskOverride !== undefined ? taskOverride : s.enabled !== false
|
|
1868
2070
|
})
|
|
1869
|
-
|
|
1870
|
-
|
|
2071
|
+
|
|
2072
|
+
// Role-based relevance filter: each role only gets skills that match its job
|
|
2073
|
+
const ROLE_SKILL_KEYWORDS = {
|
|
2074
|
+
builder: ['test', 'run', 'build', 'lint', 'commit', 'style', 'deploy', 'migration', 'seed'],
|
|
2075
|
+
scout: ['scout', 'codebase', 'explore', 'map', 'analyze', 'read'],
|
|
2076
|
+
reviewer: ['review', 'security', 'audit', 'test', 'quality'],
|
|
2077
|
+
coordinator: ['decompose', 'plan', 'coordinate', 'parallel'],
|
|
2078
|
+
}
|
|
2079
|
+
const roleKeywords = effectiveRole ? (ROLE_SKILL_KEYWORDS[effectiveRole] || []) : []
|
|
2080
|
+
|
|
2081
|
+
// Also use the task type's suggestedSkills list for relevance
|
|
2082
|
+
const taskTypeSkills = (() => {
|
|
2083
|
+
try {
|
|
2084
|
+
const taskType = ctx.task?.taskType || 'feature'
|
|
2085
|
+
const cfg = TASK_TYPES?.[taskType]
|
|
2086
|
+
return cfg?.suggestedSkills || []
|
|
2087
|
+
} catch { return [] }
|
|
2088
|
+
})()
|
|
2089
|
+
|
|
2090
|
+
const relevantSkills = enabledSkills.filter(s => {
|
|
2091
|
+
// Always include explicitly overridden-true skills
|
|
2092
|
+
if (taskSkillOverrides[s.name] === true) return true
|
|
2093
|
+
// Include if skill name matches task type's suggested list
|
|
2094
|
+
if (taskTypeSkills.includes(s.name)) return true
|
|
2095
|
+
// Include if skill name/description matches role keywords
|
|
2096
|
+
const haystack = `${s.name} ${s.description || ''}`.toLowerCase()
|
|
2097
|
+
return roleKeywords.some(kw => haystack.includes(kw))
|
|
2098
|
+
})
|
|
2099
|
+
|
|
2100
|
+
const suggestedSkills = relevantSkills.length > 0
|
|
2101
|
+
? relevantSkills.map(s => ({
|
|
1871
2102
|
name: s.name,
|
|
1872
2103
|
description: s.description,
|
|
1873
2104
|
path: `.cursor/skills/${s.name}.md`,
|
|
@@ -1889,7 +2120,12 @@ Returns systemPrompt ready to use as a Claude system prompt.`,
|
|
|
1889
2120
|
suggestedSkills,
|
|
1890
2121
|
task: ctx.task,
|
|
1891
2122
|
project: ctx.project,
|
|
1892
|
-
usage:
|
|
2123
|
+
usage: [
|
|
2124
|
+
'Use systemPrompt as your Claude system prompt.',
|
|
2125
|
+
'If allowedTools is set, restrict your MCP tool calls to that list.',
|
|
2126
|
+
'Read ONLY the skills listed in suggestedSkills — they are filtered for your role and task type.',
|
|
2127
|
+
'After the task is done (park_task or raise_pr), the skill files are automatically deleted from .cursor/skills/.',
|
|
2128
|
+
].join(' '),
|
|
1893
2129
|
})
|
|
1894
2130
|
}
|
|
1895
2131
|
)
|
|
@@ -3615,6 +3851,43 @@ Use this when a developer says "start task", "brief me on", or "what do I need t
|
|
|
3615
3851
|
const subtasksDone = subtasks.filter(s => s.done).length
|
|
3616
3852
|
const subtasksTotal = subtasks.length
|
|
3617
3853
|
|
|
3854
|
+
// ── Gather local git state (used in both preview and confirmed path) ────────
|
|
3855
|
+
let localGitState = null
|
|
3856
|
+
{
|
|
3857
|
+
const pCwd = repoPath || process.cwd()
|
|
3858
|
+
const pRoot = findRepoRoot(pCwd)
|
|
3859
|
+
if (pRoot) {
|
|
3860
|
+
try {
|
|
3861
|
+
const porcelain = runGit('status --porcelain=v1', pRoot)
|
|
3862
|
+
const { localState, modified, staged, unstaged, untracked } = parseGitStatus(porcelain)
|
|
3863
|
+
const currentBranch = runGit('branch --show-current', pRoot).trim()
|
|
3864
|
+
|
|
3865
|
+
// Detect default branch (main / master)
|
|
3866
|
+
let defaultBranch = 'main'
|
|
3867
|
+
try {
|
|
3868
|
+
defaultBranch = runGit('symbolic-ref refs/remotes/origin/HEAD', pRoot).trim().replace('refs/remotes/origin/', '')
|
|
3869
|
+
} catch { /* no remote HEAD — stay with 'main' */ }
|
|
3870
|
+
|
|
3871
|
+
// How far behind is the current branch from origin/<default>?
|
|
3872
|
+
let mainBehindBy = 0
|
|
3873
|
+
try {
|
|
3874
|
+
runGit('fetch origin --quiet', pRoot)
|
|
3875
|
+
mainBehindBy = parseInt(runGit(`rev-list HEAD..origin/${defaultBranch} --count`, pRoot).trim(), 10) || 0
|
|
3876
|
+
} catch { /* no remote or network — non-fatal */ }
|
|
3877
|
+
|
|
3878
|
+
localGitState = {
|
|
3879
|
+
repoRoot: pRoot,
|
|
3880
|
+
currentBranch,
|
|
3881
|
+
defaultBranch,
|
|
3882
|
+
localState, // 'clean' | 'modified' | 'untracked'
|
|
3883
|
+
modifiedFiles: modified,
|
|
3884
|
+
untrackedFiles: untracked,
|
|
3885
|
+
mainBehindBy,
|
|
3886
|
+
}
|
|
3887
|
+
} catch { /* git not available or not a git repo — non-fatal */ }
|
|
3888
|
+
}
|
|
3889
|
+
}
|
|
3890
|
+
|
|
3618
3891
|
// ── Preview: show the full plan before touching anything ──
|
|
3619
3892
|
const pendingApv = (task.approvals || []).find(a => a.state === 'pending') || null
|
|
3620
3893
|
const hasApprovedApv = (task.approvals || []).some(a => a.state === 'approved')
|
|
@@ -3833,10 +4106,24 @@ Use this when a developer says "start task", "brief me on", or "what do I need t
|
|
|
3833
4106
|
claimedFiles: task.claimedFiles || [],
|
|
3834
4107
|
agentRole: task.agentRole || null,
|
|
3835
4108
|
},
|
|
4109
|
+
localGitState: localGitState ? {
|
|
4110
|
+
currentBranch: localGitState.currentBranch,
|
|
4111
|
+
status: localGitState.localState,
|
|
4112
|
+
modifiedFiles: localGitState.modifiedFiles,
|
|
4113
|
+
untrackedFiles: localGitState.untrackedFiles,
|
|
4114
|
+
mainBehindBy: localGitState.mainBehindBy,
|
|
4115
|
+
...(localGitState.localState === 'modified' && {
|
|
4116
|
+
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.`,
|
|
4117
|
+
suggestedFix: `git stash push -m "wip: before starting ${task.key}"`,
|
|
4118
|
+
}),
|
|
4119
|
+
...(localGitState.mainBehindBy > 0 && {
|
|
4120
|
+
mainWarning: `⚠️ Your workspace is ${localGitState.mainBehindBy} commit(s) behind origin/${localGitState.defaultBranch}. Run: git pull origin ${localGitState.defaultBranch} before creating a branch.`,
|
|
4121
|
+
}),
|
|
4122
|
+
} : null,
|
|
3836
4123
|
requiresConfirmation: true,
|
|
3837
4124
|
message: approvalBlocks
|
|
3838
4125
|
? `Read the plan above, then follow workflowRoadmap — approval is required before you can branch and start coding.`
|
|
3839
|
-
: `Read the implementation plan above carefully, then call kickoff_task again with confirmed=true.`,
|
|
4126
|
+
: `Read the implementation plan above carefully, resolve any localGitState warnings, then call kickoff_task again with confirmed=true.`,
|
|
3840
4127
|
})
|
|
3841
4128
|
}
|
|
3842
4129
|
|
|
@@ -3894,28 +4181,39 @@ Use this when a developer says "start task", "brief me on", or "what do I need t
|
|
|
3894
4181
|
}
|
|
3895
4182
|
|
|
3896
4183
|
// ── #1 Preflight: check dirty tree before writing cursor rules / moving task ──
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
const
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
4184
|
+
// Use the git state we already gathered above; re-check if not available.
|
|
4185
|
+
if (localGitState?.localState === 'modified') {
|
|
4186
|
+
const { currentBranch, modifiedFiles } = localGitState
|
|
4187
|
+
const taskBranch = task.github?.headBranch
|
|
4188
|
+
|
|
4189
|
+
if (taskBranch && currentBranch !== taskBranch) {
|
|
4190
|
+
// On the wrong branch — stash and switch, or cancel
|
|
4191
|
+
return text({
|
|
4192
|
+
blocked: true,
|
|
4193
|
+
reason: `You are on "${currentBranch}" with ${modifiedFiles.length} uncommitted change(s), but this task's branch is "${taskBranch}".`,
|
|
4194
|
+
modifiedFiles,
|
|
4195
|
+
choices: {
|
|
4196
|
+
stash: `git stash push -m "wip: before kickoff ${task.key} on ${currentBranch}"`,
|
|
4197
|
+
autoSwitch: `git stash && git checkout ${taskBranch} && git stash pop`,
|
|
4198
|
+
cancel: 'Review your changes first, then retry kickoff_task',
|
|
4199
|
+
},
|
|
4200
|
+
message: '⛔ Resolve the working tree before kicking off this task to avoid cross-task pollution.',
|
|
4201
|
+
})
|
|
4202
|
+
}
|
|
4203
|
+
|
|
4204
|
+
if (!taskBranch) {
|
|
4205
|
+
// First-time kickoff — uncommitted changes risk bleeding into the new branch
|
|
4206
|
+
return text({
|
|
4207
|
+
blocked: true,
|
|
4208
|
+
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.`,
|
|
4209
|
+
modifiedFiles,
|
|
4210
|
+
choices: {
|
|
4211
|
+
stash: `git stash push -m "wip: before starting ${task.key}"`,
|
|
4212
|
+
commit: 'git add -p && git commit -m "wip: save changes before new task"',
|
|
4213
|
+
cancel: 'Review your changes, then retry kickoff_task',
|
|
4214
|
+
},
|
|
4215
|
+
message: '⛔ Commit or stash your changes before creating a new task branch.',
|
|
4216
|
+
})
|
|
3919
4217
|
}
|
|
3920
4218
|
}
|
|
3921
4219
|
|
|
@@ -4091,6 +4389,26 @@ After \`request_human_input\`: STOP, show the question in chat, wait for reply,
|
|
|
4091
4389
|
}
|
|
4092
4390
|
api.patch(`/api/tasks/${taskId}`, workspacePatch).catch(() => {/* non-fatal */})
|
|
4093
4391
|
|
|
4392
|
+
// Auto-log every skill / rule / agent written to the session timeline
|
|
4393
|
+
// so the Session tab always shows what got injected at kickoff.
|
|
4394
|
+
if (workspaceResult?.written?.length) {
|
|
4395
|
+
const repoRoot = workspaceResult.repoRoot || ''
|
|
4396
|
+
for (const filePath of workspaceResult.written) {
|
|
4397
|
+
const rel = filePath.replace(repoRoot, '').replace(/^\//, '')
|
|
4398
|
+
const name = rel.replace(/^\.cursor\/(skills|rules|agents|commands)\//, '').replace(/\.(md|mdc)$/, '')
|
|
4399
|
+
const type = rel.includes('/skills/') ? 'skill'
|
|
4400
|
+
: rel.includes('/rules/') ? 'rule'
|
|
4401
|
+
: rel.includes('/agents/') ? 'subagent'
|
|
4402
|
+
: 'info'
|
|
4403
|
+
api.post(`/api/tasks/${taskId}/session/log`, {
|
|
4404
|
+
type,
|
|
4405
|
+
name,
|
|
4406
|
+
role: agentRole || null,
|
|
4407
|
+
summary: `${type === 'skill' ? '📖' : type === 'rule' ? '📏' : type === 'subagent' ? '🤖' : 'ℹ️'} Injected at kickoff: ${rel}`,
|
|
4408
|
+
}).catch(() => {/* non-fatal */})
|
|
4409
|
+
}
|
|
4410
|
+
}
|
|
4411
|
+
|
|
4094
4412
|
// Write .internaltool-active-task so the Claude Code hook knows a task is active
|
|
4095
4413
|
try {
|
|
4096
4414
|
writeFileSync(
|
|
@@ -4345,18 +4663,40 @@ How to determine status:
|
|
|
4345
4663
|
|
|
4346
4664
|
server.tool(
|
|
4347
4665
|
'submit_task_for_approval',
|
|
4348
|
-
|
|
4666
|
+
`Create and immediately submit an approval request so a human reviewer can approve the plan before any branch or code is created.
|
|
4667
|
+
|
|
4668
|
+
This is a single atomic operation — it creates the approval draft and submits it in one step.
|
|
4669
|
+
Only one approval can be pending at a time. Returns blocked=true if another is already pending.
|
|
4670
|
+
|
|
4671
|
+
IMPORTANT — what to write in "summary":
|
|
4672
|
+
- Write a SHORT, new approval request (3–8 sentences). Do NOT copy the task readmeMarkdown verbatim.
|
|
4673
|
+
- 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.
|
|
4674
|
+
- 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()."`,
|
|
4349
4675
|
{
|
|
4350
4676
|
taskId: z.string().describe("Task's MongoDB ObjectId"),
|
|
4351
|
-
title: z.string().describe('Short title for this approval request, e.g. "
|
|
4352
|
-
|
|
4353
|
-
reviewerId: z.string().describe('User ID of the reviewer'),
|
|
4677
|
+
title: z.string().describe('Short title for this approval request, e.g. "Plan: Add Redis caching to product list"'),
|
|
4678
|
+
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.'),
|
|
4679
|
+
reviewerId: z.string().describe('User ID of the reviewer (admin or project lead)'),
|
|
4354
4680
|
},
|
|
4355
|
-
async ({ taskId, title,
|
|
4356
|
-
// Gate: block if
|
|
4681
|
+
async ({ taskId, title, summary, reviewerId }) => {
|
|
4682
|
+
// Gate: block if another approval is already pending
|
|
4357
4683
|
const taskRes = await api.get(`/api/tasks/${taskId}`)
|
|
4358
4684
|
if (taskRes?.success) {
|
|
4359
|
-
const
|
|
4685
|
+
const task = taskRes.data?.task
|
|
4686
|
+
const alreadyPending = (task?.approvals || []).find(a => a.state === 'pending')
|
|
4687
|
+
if (alreadyPending) {
|
|
4688
|
+
return text({
|
|
4689
|
+
blocked: true,
|
|
4690
|
+
reason: 'An approval is already pending review.',
|
|
4691
|
+
pendingApproval: {
|
|
4692
|
+
title: alreadyPending.title,
|
|
4693
|
+
requestedAt: alreadyPending.requestedAt,
|
|
4694
|
+
},
|
|
4695
|
+
message: 'Wait for the reviewer to decide on the existing request, or ask them to reject it first.',
|
|
4696
|
+
})
|
|
4697
|
+
}
|
|
4698
|
+
// Gate: block if latest test run is failing
|
|
4699
|
+
const latestRun = task?.testRuns?.[0]
|
|
4360
4700
|
if (latestRun && latestRun.status === 'failing') {
|
|
4361
4701
|
return text({
|
|
4362
4702
|
blocked: true,
|
|
@@ -4372,7 +4712,33 @@ How to determine status:
|
|
|
4372
4712
|
})
|
|
4373
4713
|
}
|
|
4374
4714
|
}
|
|
4375
|
-
|
|
4715
|
+
|
|
4716
|
+
// Step 1: create draft
|
|
4717
|
+
const createRes = await api.post(`/api/tasks/${taskId}/approvals`, { title, readme: summary })
|
|
4718
|
+
if (!createRes?.success) return errorText(createRes?.message || 'Could not create approval draft')
|
|
4719
|
+
const freshTask = createRes.data?.task
|
|
4720
|
+
const newApproval = (freshTask?.approvals || []).find(a => a.state === 'none' && a.title === title)
|
|
4721
|
+
if (!newApproval?._id) return errorText('Approval created but could not find its ID — check the task approvals tab')
|
|
4722
|
+
|
|
4723
|
+
// Step 2: immediately submit for review
|
|
4724
|
+
const submitRes = await api.post(`/api/tasks/${taskId}/approvals/${newApproval._id}/submit`, { reviewerId })
|
|
4725
|
+
if (!submitRes?.success) {
|
|
4726
|
+
return text({
|
|
4727
|
+
draftCreated: true,
|
|
4728
|
+
approvalId: newApproval._id,
|
|
4729
|
+
submitFailed: true,
|
|
4730
|
+
reason: submitRes?.message || 'Draft created but submit step failed',
|
|
4731
|
+
message: 'The approval draft was created but could not be submitted automatically. Open the task → Approval tab → click "Submit for review" manually.',
|
|
4732
|
+
})
|
|
4733
|
+
}
|
|
4734
|
+
return text({
|
|
4735
|
+
submitted: true,
|
|
4736
|
+
approvalId: newApproval._id,
|
|
4737
|
+
title,
|
|
4738
|
+
state: 'pending',
|
|
4739
|
+
message: `Approval request submitted. The reviewer will be notified. Do NOT start coding or create a branch until they approve.`,
|
|
4740
|
+
nextStep: `Wait for decide_task_approval to return decision="approve". Then call create_branch.`,
|
|
4741
|
+
})
|
|
4376
4742
|
}
|
|
4377
4743
|
)
|
|
4378
4744
|
|
|
@@ -5075,6 +5441,29 @@ function writeCursorWorkspace(task, projectAgentConfig, startPath) {
|
|
|
5075
5441
|
mkdirSync(commandsDir, { recursive: true })
|
|
5076
5442
|
mkdirSync(rulesDir, { recursive: true })
|
|
5077
5443
|
|
|
5444
|
+
// ── Ensure generated AI files are NOT committed by the developer ────────────
|
|
5445
|
+
// These are runtime files — they change per task and per agent session.
|
|
5446
|
+
// They belong in the developer's local env, not in git history.
|
|
5447
|
+
const gitignorePath = join(repoRoot, '.gitignore')
|
|
5448
|
+
const AI_GITIGNORE_ENTRIES = [
|
|
5449
|
+
'# InternalTool AI workspace — auto-generated, do not commit',
|
|
5450
|
+
'.cursor/agents/',
|
|
5451
|
+
'.cursor/skills/',
|
|
5452
|
+
'.cursor/commands/',
|
|
5453
|
+
'.cursor/rules/',
|
|
5454
|
+
'.claude/',
|
|
5455
|
+
'.internaltool-active-task',
|
|
5456
|
+
]
|
|
5457
|
+
try {
|
|
5458
|
+
let existing = ''
|
|
5459
|
+
if (existsSync(gitignorePath)) existing = readFileSync(gitignorePath, 'utf8')
|
|
5460
|
+
const missing = AI_GITIGNORE_ENTRIES.filter(e => e.startsWith('#') ? false : !existing.includes(e))
|
|
5461
|
+
if (missing.length > 0) {
|
|
5462
|
+
const toAppend = '\n' + AI_GITIGNORE_ENTRIES[0] + '\n' + missing.join('\n') + '\n'
|
|
5463
|
+
writeFileSync(gitignorePath, existing + toAppend, 'utf8')
|
|
5464
|
+
}
|
|
5465
|
+
} catch { /* non-fatal — gitignore update is best-effort */ }
|
|
5466
|
+
|
|
5078
5467
|
const written = []
|
|
5079
5468
|
|
|
5080
5469
|
// ── 1. Project-level rules from DB → .cursor/rules/<name>.mdc ─────────────
|
|
@@ -5340,23 +5729,41 @@ log_session_event(type="info", name="conflict-resolved", summary="<what you merg
|
|
|
5340
5729
|
|
|
5341
5730
|
/**
|
|
5342
5731
|
* Delete task-specific workspace files written at kickoff.
|
|
5343
|
-
*
|
|
5344
|
-
*
|
|
5732
|
+
* Removes: active-agent.md, start-task.md command, and ALL skill files in .cursor/skills/.
|
|
5733
|
+
* Project-level rules (.cursor/rules/*.mdc) are kept — they are curated by the admin, not generated per task.
|
|
5345
5734
|
*/
|
|
5346
5735
|
function deleteCursorWorkspace(role, startPath) {
|
|
5347
5736
|
const deleted = []
|
|
5348
5737
|
try {
|
|
5349
5738
|
const repoRoot = findRepoRoot(startPath)
|
|
5350
5739
|
if (!repoRoot) return deleted
|
|
5351
|
-
|
|
5740
|
+
|
|
5741
|
+
// Always-delete: task-scoped agent and command files
|
|
5742
|
+
const alwaysDelete = [
|
|
5352
5743
|
join(repoRoot, '.cursor', 'agents', 'active-agent.md'),
|
|
5353
5744
|
join(repoRoot, '.cursor', 'commands', 'start-task.md'),
|
|
5354
5745
|
]
|
|
5355
|
-
for (const f of
|
|
5746
|
+
for (const f of alwaysDelete) {
|
|
5356
5747
|
try {
|
|
5357
5748
|
if (existsSync(f)) { unlinkSync(f); deleted.push(f) }
|
|
5358
5749
|
} catch { /* non-fatal */ }
|
|
5359
5750
|
}
|
|
5751
|
+
|
|
5752
|
+
// Delete all skill files — they are fetched fresh on every kickoff
|
|
5753
|
+
// based on role and task type. Keeping stale skills causes context bloat.
|
|
5754
|
+
const skillsDir = join(repoRoot, '.cursor', 'skills')
|
|
5755
|
+
if (existsSync(skillsDir)) {
|
|
5756
|
+
try {
|
|
5757
|
+
const skillFiles = readdirSync(skillsDir).filter(f => f.endsWith('.md'))
|
|
5758
|
+
for (const f of skillFiles) {
|
|
5759
|
+
try {
|
|
5760
|
+
const fullPath = join(skillsDir, f)
|
|
5761
|
+
unlinkSync(fullPath)
|
|
5762
|
+
deleted.push(fullPath)
|
|
5763
|
+
} catch { /* non-fatal */ }
|
|
5764
|
+
}
|
|
5765
|
+
} catch { /* non-fatal */ }
|
|
5766
|
+
}
|
|
5360
5767
|
} catch { /* non-fatal */ }
|
|
5361
5768
|
return deleted
|
|
5362
5769
|
}
|
|
@@ -6011,14 +6418,13 @@ If you have uncommitted tracked changes, it will tell you exactly what to do bef
|
|
|
6011
6418
|
const task = taskRes.data.task
|
|
6012
6419
|
|
|
6013
6420
|
// ── Approval gate check ───────────────────────────────────────────────────
|
|
6014
|
-
//
|
|
6015
|
-
// Detect this early and guide the developer instead of failing silently after branch creation.
|
|
6421
|
+
// Block branch creation (at both confirmed=false AND confirmed=true) until the plan is approved.
|
|
6016
6422
|
const pendingApv2 = (task.approvals || []).find(a => a.state === 'pending') || null
|
|
6017
6423
|
const hasApprovedApv2 = (task.approvals || []).some(a => a.state === 'approved')
|
|
6018
6424
|
const approvalState = pendingApv2 ? 'pending' : hasApprovedApv2 ? 'approved' : 'none'
|
|
6019
6425
|
const PLANNING_COLS = ['backlog', 'todo']
|
|
6020
6426
|
const needsApproval = PLANNING_COLS.includes(task.column) && !hasApprovedApv2
|
|
6021
|
-
if (needsApproval
|
|
6427
|
+
if (needsApproval) {
|
|
6022
6428
|
const isFix2 = /\b(fix|bug|hotfix|patch)\b/i.test(task.title + ' ' + (task.description || ''))
|
|
6023
6429
|
const slug2 = task.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 35)
|
|
6024
6430
|
const previewBranch = `${isFix2 ? 'fix' : 'feature'}/${task.key.toLowerCase()}-${slug2}`
|
|
@@ -6026,16 +6432,16 @@ If you have uncommitted tracked changes, it will tell you exactly what to do bef
|
|
|
6026
6432
|
blocked: true,
|
|
6027
6433
|
reason: 'approval_required',
|
|
6028
6434
|
task: { key: task.key, title: task.title, column: task.column, approvalState },
|
|
6029
|
-
message:
|
|
6435
|
+
message: `⛔ The plan must be approved before creating a branch. Do NOT code until the reviewer approves.`,
|
|
6030
6436
|
approvalStatus: approvalState === 'pending'
|
|
6031
|
-
? `Plan is
|
|
6437
|
+
? `Plan is submitted and waiting for reviewer approval. Once approved, call create_branch again.`
|
|
6032
6438
|
: `Plan has not been submitted for review yet.`,
|
|
6033
6439
|
nextSteps: approvalState === 'pending'
|
|
6034
|
-
? [`Wait for
|
|
6440
|
+
? [`Wait for decide_task_approval to return decision="approve", then call create_branch with taskId="${taskId}"`]
|
|
6035
6441
|
: [
|
|
6036
|
-
`1.
|
|
6037
|
-
`2. Call submit_task_for_approval with taskId="${taskId}"
|
|
6038
|
-
`3. Once approved
|
|
6442
|
+
`1. Write the implementation plan: call update_task with readmeMarkdown if not already written`,
|
|
6443
|
+
`2. Call submit_task_for_approval with taskId="${taskId}", summary="<3-8 sentence summary>", reviewerId="<admin user id>"`,
|
|
6444
|
+
`3. Once approved: call create_branch — it will create "${previewBranch}" and move the task to In progress`,
|
|
6039
6445
|
],
|
|
6040
6446
|
})
|
|
6041
6447
|
}
|
package/package.json
CHANGED