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.
Files changed (2) hide show
  1. package/index.js +1060 -254
  2. 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 — analyzes the codebase and creates a fully structured, kickoff-ready task.
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 (always first)
527
+ ### Step 1 — Duplicate check
531
528
  Call search_tasks(projectId, query="<keywords from the request>").
532
- If a similar open task already exists → return it with kickoff instructions instead of creating a duplicate.
529
+ If a similar open task exists → return it with kickoff instructions. Do NOT create a duplicate.
533
530
 
534
- ### Step 2 — Codebase analysis (READ the code, do not guess)
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
- Identify: language, framework, major libraries, test runner.
539
-
540
- b) **Entry point mapping** — find where the relevant feature area lives:
541
- - For backend: grep for existing route patterns (router.post, app.get, @Controller, etc.)
542
- - For frontend: grep for existing component patterns, hooks, state management
543
- - For DB: find schema/model files
544
-
545
- c) **Pattern extraction** read 2-3 existing files similar to what you'll build.
546
- Note: naming conventions, folder structure, how services/routes/components are wired.
547
-
548
- d) **Impact analysis** identify every file that needs to change:
549
- - Files to CREATE (new route, new component, new model, new test)
550
- - Files to MODIFY (existing router index, existing schema, existing types)
551
-
552
- e) **Dependency order** which files must be built first (schema before service, service before route, etc.)
553
-
554
- ### Step 3 Write the implementation plan
555
- Using what you found in Step 2, build:
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 — language/framework detected
558
- - ## Technical approach — how it fits into the existing code (name actual files and functions)
559
- - ## Files to create — path + what it does
560
- - ## Files to modify — path + what changes
561
- - ## Subtasks — ordered implementation steps (schema → service → route → test → UI)
562
- - ## Acceptance criteria — what done looks like
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 (from this call)
572
- - title: action-oriented, concise (verb + noun, e.g. "Add rate limiting to /api/auth/login")
573
- - description: one paragraph summary
574
- - readmeMarkdown: the full plan from Step 3
575
- - taskType, priority, column="todo"
576
- - subtasks: the ordered list from Step 3 (each step = one subtask)
577
- - suggestedFiles: from Step 2d
578
-
579
- After create_task succeeds, immediately call:
580
- kickoff_task(taskId=<returned id>, confirmed=true, agentRole="builder", files=<suggestedFiles>)
581
-
582
- ## What makes a high-quality task
583
- - readmeMarkdown references REAL file paths found by grepping the codebase
584
- - subtasks are ordered (schema first, then service, then route, then test, then UI)
585
- - suggestedFiles lists every file that will be touched — no omissions
586
- - title is specific ("Add email verification to /api/auth/register") not generic ("Add email feature")
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. Return codebase intelligence + tight instructions ──────────────────
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 — use codebaseIntelligence above to skip re-reading the filesystem
778
+ // What to do now
660
779
  nextSteps: [
661
- 'Use dirTree + entryPoints above to identify which files need changing — you already have the map.',
662
- `Read 2-3 files from entryPoints.routes / entryPoints.models / entryPoints.components to extract naming conventions and wiring patterns.`,
663
- 'Draft readmeMarkdown with: ## Goal, ## Stack, ## Technical approach (name real files), ## Files to create, ## Files to modify, ## Subtasks (ordered), ## Acceptance criteria.',
664
- `Call create_task(projectId="${projectId}", title="<verb + noun>", readmeMarkdown="<plan>", taskType="<feature|bugfix|...>", priority="${priority}", column="todo", subtasks=[...], suggestedFiles=[...])`,
665
- 'Immediately after: call kickoff_task(taskId=<returned id>, confirmed=true, agentRole="builder", files=[...suggestedFiles])',
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: true,
741
- taskId: task?._id,
742
- taskKey: task?.key,
743
- title: task?.title,
744
- column: task?.column,
745
- taskType: task?.taskType || null,
746
- subtasks: (task?.subtasks || []).length,
747
- kickoff: task?._id
748
- ? `kickoff_task(taskId="${task._id}", confirmed=false) ← read plan first\nkickoff_task(taskId="${task._id}", confirmed=true, agentRole="builder", files=[...]) ← start building`
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 and ready to kick off.`,
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 the cursor rules file (.cursor/rules/<task>.mdc) so it doesn't bleed into other tasks
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 commitMsg = `wip(${task?.key?.toLowerCase() || 'task'}): ${(summary || 'parking task').slice(0, 60)}`
879
- runGit('add -A', repoRoot)
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 rules file (.cursor/rules/<task>.mdc) so Cursor follows task rules
943
- 6. Posts a comment that you picked up the task
944
- 7. Notifies the previous developer that you have taken over
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 rules restored${agentRole ? ` (role: ${agentRole})` : ''}. Start coding from where ${task?.parkNote?.parkedBy ? 'the previous developer' : 'you'} left off.`
1101
- : `Branch switch failed — see git.manualSteps. Cursor rules restored.`,
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
- ? `BUILDER role active. Call claim_files to lock your files before editing.`
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
- : null,
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 ? '⚠️ No scout report on this task. Consider running a scout agent first to map the codebase before builders start.' : null,
1393
- !executionPlan.readmePresent ? '⚠️ No implementation plan (README). Builders need a spec to work from.' : null,
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: 'todo',
1421
- priority: task.priority || 'medium',
1422
- taskType: s.role === 'reviewer' ? 'feature' : (task.taskType || 'feature'),
1423
- parentTask: taskId,
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
- createdTasks.push({
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
- Use agentRole="scout" in kickoff_task first, then call scout_task to get your briefing.
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 context)
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
- instruction: 'Please reply in this Cursor chat. I will use your answer to continue. Do NOT proceed until you reply.',
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
- // Suggest relevant skills based on role and available project skills
1916
- // Respects project-level enabled + task-level agentKitOverrides
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 availableSkills = allProjectSkills.filter(s => {
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
- const suggestedSkills = availableSkills.length > 0
1924
- ? availableSkills.map(s => ({
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: 'Use systemPrompt as your Claude system prompt. If allowedTools is set, restrict your MCP tool calls to that list. Read and follow each skill in suggestedSkills before starting work.',
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 = ['looks good', 'lgtm', 'code is fine', 'no issues', 'all good', 'seems correct']
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 < 20 || genericPhrases.some(g => p.toLowerCase().includes(g))
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
- { check: 'Has at least one approval', pass: pr.approvals > 0,
2228
- note: pr.approvals === 0 ? 'No approvals yet consider running post_pr_review first' : `${pr.approvals} approval(s)` },
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}" will move to Done automatically.`,
2284
- nextStep: `The GitHub webhook will update the board. If the task doesn't move to Done within a minute, call update_task with column="done" manually.`,
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}". Ready to resume coding${agentRole ? ` as ${agentRole}` : ''}.`
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
- ? `BUILDER role active. Call claim_files to lock your files before editing.`
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
- // Determine which group to run next: first group that has at least one
2584
- // subtask not yet reflected in the board subtasks as done.
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
- // Board subtask titles may carry a role prefix like "[BUILDER] Auth hardening..."
2587
- // Match on bare title OR prefixed title
2588
- const isDone = (title) => boardSubtasks.some(s => s.done && (s.title === title || s.title.endsWith(title)))
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.every(t => isDone(t))
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 priorAllDone = execOrder.slice(0, i).every(g => g.every(t => isDone(t)))
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.filter(t => !isDone(t))
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}" for task ${task.key}.`,
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 "${taskId}", type "subagent", name "builder-${i + 1}", summary "Builder ${i + 1} started: ${st.title}"`,
2644
- `2. Call kickoff_task with taskId "${taskId}", agentRole "builder", confirmed true`,
2645
- `3. Call claim_files with taskId "${taskId}" and files [${filesArg}]`,
2646
- `4. Call get_agent_context with taskId "${taskId}" to read the full task plan and suggested skills`,
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 any skills listed in get_agent_context suggestedSkills`,
2651
- `6. Run npm test — all tests must pass before continuing`,
2652
- `7. Call commit_helper with taskId "${taskId}"`,
2653
- `8. Call update_task with taskId "${taskId}" and mark the subtask "${st.title}" as done`,
2654
- `9. Call resume_task with taskId "${taskId}", agentRole "reviewer", confirmed true transition to reviewer role`,
2655
- `10. Call log_session_event with taskId "${taskId}", type "subagent", name "builder-${i + 1}", summary "Builder ${i + 1} finished: ${st.title}"`,
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. Another builder is working on different files at the same time.`,
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 calls kickoff_taskclaim_files → implements → runs tests → commits.`,
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 = dec.executionOrder || []
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
- const doneTitles = targetGroup.filter(t => isDone(t))
2761
- const pendingTitles = targetGroup.filter(t => !isDone(t))
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: true,
2766
- group: groupIndex,
2767
- subtasks: targetGroup,
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 = (finalDec.executionOrder || [])[groupIndex - 1] || []
2797
- const finalBoard = finalTask?.subtasks || []
2798
- const finalIsDone = (t) => finalBoard.some(s => s.done && (s.title === t || s.title.endsWith(t)))
2799
- const stillPending = finalGroup.filter(t => !finalIsDone(t))
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 waiting, or proceed manually.',
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
- const pCwd = repoPath || process.cwd()
3953
- const pRoot = findRepoRoot(pCwd)
3954
- if (pRoot) {
3955
- try {
3956
- const porcelain = runGit('status --porcelain=v1', pRoot)
3957
- const { localState, modified } = parseGitStatus(porcelain)
3958
- const currentBranchNow = runGit('branch --show-current', pRoot).trim()
3959
- if (localState === 'modified' && task.github?.headBranch && currentBranchNow !== task.github.headBranch) {
3960
- return text({
3961
- blocked: true,
3962
- reason: `You are on "${currentBranchNow}" with ${modified.length} modified file(s), but this task's branch is "${task.github.headBranch}".`,
3963
- modifiedFiles: modified,
3964
- choices: {
3965
- stash: `git stash push -m "wip: before kickoff ${task.key} on ${currentBranchNow}"`,
3966
- autoSwitch: `Switch to ${task.github.headBranch}: git stash && git checkout ${task.github.headBranch} && git stash pop`,
3967
- cancel: 'Review your changes first, then retry kickoff_task',
3968
- },
3969
- message: 'Resolve the working tree before kicking off this task to avoid cross-task pollution.',
3970
- })
3971
- }
3972
- } catch { /* non-fatal */ }
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
- nextStep: agentRole === 'scout'
4211
- ? `SCOUT mode. Read the codebase, then save findings with update_task(scoutReport=...). Do NOT modify files.`
4212
- : agentRole === 'coordinator'
4213
- ? `COORDINATOR mode. Read the README, then call decompose_task to split work into parallel/sequential subtasks with file ownership.`
4214
- : agentRole === 'builder'
4215
- ? `BUILDER mode. Call claim_files first, then start coding on "${alreadyHasBranch ? task.github.headBranch : suggestedBranch}".`
4216
- : agentRole === 'reviewer'
4217
- ? `REVIEWER mode. Call review_pr to get the diff, then post_pr_review with your analysis.`
4218
- : recommendedFlow === 'coordinator' && shouldAutoRoute
4219
- ? `⚡ COMPLEX TASK (score ${complexityScore}): Call kickoff_task with agentRole "coordinator" confirmed true → decompose_task → get_parallel_kickoffs → start builders. DO NOT code directly.`
4220
- : recommendedFlow === 'single_builder' && shouldAutoRoute
4221
- ? `🔧 MEDIUM TASK (score ${complexityScore}): Call kickoff_task with agentRole "builder" confirmed true → claim_files → implement → commit → raise_pr.`
4222
- : alreadyHasBranch
4223
- ? `SIMPLE TASK (score ${complexityScore}). Branch "${task.github.headBranch}" existscall claim_files and start coding.`
4224
- : `SIMPLE TASK (score ${complexityScore}). Call create_branch to create "${suggestedBranch}", then claim_files and code.`,
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
- 'Create and submit a new approval request on a task. Each request has its own title, plan/readme, and reviewer. Only one request can be pending at a time.',
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. "Experiment: Add caching layer"'),
4406
- readme: z.string().describe('The plan/markdown describing what you want to do and why (min 80 chars)'),
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, readme, reviewerId }) => {
4410
- // Gate: block if latest test run is failing
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 latestRun = taskRes.data?.task?.testRuns?.[0]
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
- return call(() => api.post(`/api/tasks/${taskId}/approvals`, { title, readme, reviewerId }))
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
- 'Approve or reject a specific approval request by approvalId. Only the designated reviewer can call this.',
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
- call(() => api.post(`/api/tasks/${taskId}/approvals/${approvalId}/decide`, { decision, note }))
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
- * Project-level rules (.cursor/rules/<project-rule>.mdc) and skills are kept.
5398
- * Only the active-agent and start-task command are removed (they are task-scoped).
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
- const toDelete = [
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 toDelete) {
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
- return { error: true, cause: e._cause || 'unknown', message: e.message?.split('\n')[0] }
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
- // The server blocks non-admins from moving todo in_progress without approval.
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 && !confirmed) {
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
- const previewBranch = `${isFix2 ? 'fix' : 'feature'}/${task.key.toLowerCase()}-${slug2}`
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: `The task's implementation plan (README) must be approved before creating a branch and moving to In progress.`,
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 already submitted and waiting for reviewer approval. Once approved, call create_branch again.`
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 the reviewer to approve, then call create_branch with taskId="${taskId}"`]
6873
+ ? [`Wait for decide_task_approval to return decision="approve", then call create_branch with taskId="${taskId}"`]
6089
6874
  : [
6090
- `1. Make sure the README / implementation plan is written on the task (use update_task with readmeMarkdown if needed)`,
6091
- `2. Call submit_task_for_approval with taskId="${taskId}" and the reviewerId of your project lead`,
6092
- `3. Once approved, call create_branch again — it will create "${previewBranch}" and move the task to In progress automatically`,
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 task-specific cursor rules file — coding is done
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
- cursorRulesCleared: deletedRulesFile
6674
- ? { cleared: true, path: deletedRulesFile, note: 'Task-specific Cursor rules file deleted — coding is complete.' }
6675
- : { cleared: false },
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
- : 'PR is live. The GitHub webhook will move the task to in_review and notify the reviewer within seconds.',
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: `Request review on PR #${branch.prNumber}`,
7414
- why: `Your PR is already open. After pushing the rebase, request reviewers so they can approve and merge.`,
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: 'Request review via GitHub CLI', cmd: `gh pr review ${branch.prNumber} --request-changes` },
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 = execSync(`git -C "${repoPath}" status --short 2>/dev/null`, { encoding: 'utf8' }).trim()
7901
- const logLine = execSync(`git -C "${repoPath}" log --oneline -1 2>/dev/null`, { encoding: 'utf8' }).trim()
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
  }