diff --git a/.github/skills/example-data-pipeline-skill.yaml b/.github/skills/example-data-pipeline-skill.yaml new file mode 100644 index 0000000..8a82001 --- /dev/null +++ b/.github/skills/example-data-pipeline-skill.yaml @@ -0,0 +1,53 @@ +name: data-pipeline +description: Expert in ETL and data processing workflows + +instructions: | + When planning a data pipeline project, structure work into these phases: + + 1. **Data Extraction**: Source connections and data retrieval + - Set up connectors to data sources (databases, APIs, files) + - Implement scheduled or event-driven extraction + - Handle authentication and connection pooling + - Add retry logic and error handling + + 2. **Data Transformation**: Processing and enrichment + - Design transformation logic (cleaning, validation, enrichment) + - Implement data quality checks + - Create reusable transformation functions + - Add data lineage tracking + + 3. **Data Loading**: Destination setup and loading + - Configure target database/warehouse schema + - Implement batch or streaming loading + - Add idempotency and deduplication + - Set up incremental loading strategies + + 4. **Monitoring & Observability**: Health and performance tracking + - Configure logging for each pipeline stage + - Set up alerts for failures and data quality issues + - Create dashboards for pipeline metrics + - Implement data reconciliation checks + + Consider idempotency, error handling, and data quality throughout. + Maximize parallelism where independent transformations can run concurrently. + +examples: + - input: "Build ETL pipeline from REST API to data warehouse" + tasks: + - Create API connector with authentication + - Implement data extraction scheduler + - Build transformation functions for data cleaning + - Design target warehouse schema + - Implement batch loading with deduplication + - Add error handling and retry logic + - Configure monitoring and alerting + - Create data quality validation tests + + - input: "Real-time streaming pipeline from Kafka to analytics DB" + tasks: + - Setup Kafka consumer with proper offset management + - Implement real-time transformation logic + - Configure target database for streaming writes + - Add exactly-once processing guarantees + - Setup monitoring dashboard + - Create backfill process for historical data diff --git a/.github/skills/example-web-app-skill.yaml b/.github/skills/example-web-app-skill.yaml new file mode 100644 index 0000000..fe4ff79 --- /dev/null +++ b/.github/skills/example-web-app-skill.yaml @@ -0,0 +1,63 @@ +name: web-app +description: Expert in web application development with frontend, backend, and deployment tasks + +instructions: | + When planning a web application project, follow these best practices: + + 1. **Frontend Tasks**: Break down UI work into distinct components + - Set up UI framework (React, Vue, Angular, etc.) + - Create reusable component library + - Implement routing and navigation + - Add state management if needed + - Style with CSS framework or custom styles + + 2. **Backend Tasks**: Structure API and business logic + - Design and implement REST/GraphQL API + - Set up database schema and migrations + - Implement authentication and authorization + - Add data validation and error handling + - Create background job processing if needed + + 3. **Database Tasks**: Plan data layer + - Design normalized database schema + - Set up database connections and pooling + - Create seed data and test fixtures + - Implement database backups + + 4. **Infrastructure & Deployment**: Prepare for production + - Configure CI/CD pipeline + - Set up environment variables and secrets + - Containerize application (Docker) + - Deploy to cloud platform (AWS, Azure, GCP, Vercel, etc.) + - Configure monitoring and logging + + 5. **Testing**: Ensure quality + - Unit tests for business logic + - Integration tests for API endpoints + - E2E tests for critical user flows + - Performance and load testing + + When generating tasks, maximize parallelism by minimizing dependencies between + frontend and backend work where possible. + +examples: + - input: "Build a task management web app" + tasks: + - Setup frontend with React and TypeScript + - Create task list component with CRUD operations + - Design REST API for task management + - Implement PostgreSQL schema for tasks and users + - Add JWT authentication + - Deploy to Vercel (frontend) and Railway (backend) + - Write E2E tests for task creation flow + + - input: "Create an e-commerce site" + tasks: + - Setup Next.js with App Router + - Create product catalog UI components + - Build shopping cart functionality + - Design product and order database schema + - Implement payment processing (Stripe) + - Add admin dashboard for product management + - Configure CDN for product images + - Set up monitoring with Sentry diff --git a/README.md b/README.md index 9987d3b..5878df6 100644 --- a/README.md +++ b/README.md @@ -53,10 +53,123 @@ planeteer list | `↑` `↓` | Navigate task list | | `⏎` | Submit input / proceed to next screen | | `Esc` | Go back | +| `⇥` | Toggle view (Tree / Batches / Skills) | +| `Space` | Toggle skill on/off (Skills view) | +| `/` | Command mode (refine screen) | | `s` | Save plan (refine screen) | | `x` | Start execution (refine/execute screen) | | `q` | Quit | +## Custom Copilot Skills + +Planeteer supports custom Copilot skills for domain-specific planning. Skills help Copilot generate better work breakdowns by providing context about specific project types. + +### Using Skills + +Skills are automatically loaded from the `.github/skills/` directory. On first run, this directory is created with example skills. To use skills: + +1. View active skills in the **Refine** screen by pressing `⇥` to cycle to the Skills view +2. Use `↑`/`↓` to navigate and `Space` to toggle skills on/off +3. Skills are applied during work breakdown generation and refinement + +### Creating Skills + +Create a new YAML file in `.github/skills/` with this structure: + +```yaml +name: my-custom-skill +description: Brief description of what this skill helps with + +instructions: | + When planning this type of project, follow these guidelines: + + 1. **Category 1**: Guidelines for this aspect + - Specific point 1 + - Specific point 2 + + 2. **Category 2**: More guidelines + - Another point + - Another point + + General advice about task structure, dependencies, etc. + +examples: + - input: "Example project description" + tasks: + - Task 1 that would be generated + - Task 2 that would be generated + - Task 3 that would be generated +``` + +### Skill Examples + +**Example 1: Web Application Skill** + +```yaml +name: web-app +description: Expert in web application development + +instructions: | + Break down web projects into frontend, backend, database, and deployment: + + 1. **Frontend**: Component structure, routing, state management + 2. **Backend**: API design, business logic, authentication + 3. **Database**: Schema design, migrations, seed data + 4. **Infrastructure**: CI/CD, containerization, cloud deployment + + Maximize parallelism between frontend and backend work. + +examples: + - input: "Build a task management web app" + tasks: + - Setup React frontend with TypeScript + - Design REST API for task CRUD + - Implement PostgreSQL schema + - Add JWT authentication + - Deploy to cloud platform +``` + +**Example 2: Data Pipeline Skill** + +```yaml +name: data-pipeline +description: Expert in ETL and data processing workflows + +instructions: | + Structure data pipelines with these phases: + + 1. **Extraction**: Data sources, connectors, scheduling + 2. **Transformation**: Cleaning, validation, enrichment + 3. **Loading**: Destination setup, batch vs streaming + 4. **Monitoring**: Logging, alerts, data quality checks + + Consider idempotency, error handling, and reprocessing. + +examples: + - input: "Build ETL pipeline from API to data warehouse" + tasks: + - Implement API data extractor + - Create transformation functions + - Setup data warehouse schema + - Add error handling and retries + - Configure monitoring and alerts +``` + +### Skill Best Practices + +- **One skill per domain**: Create focused skills (e.g., `mobile-app`, `ml-pipeline`) rather than generic ones +- **Clear instructions**: Be specific about task breakdown patterns and dependencies +- **Provide examples**: Include 2-3 representative examples with typical task structures +- **Enable selectively**: Toggle skills on/off based on your current project type + +### Built-in Example + +Two example skills are included in the repository to help you get started: +- **example-web-app-skill.yaml** - Web application development best practices +- **example-data-pipeline-skill.yaml** - ETL and data processing workflow patterns + +These files are automatically available in `.github/skills/` and can be used as templates for creating your own custom skills. + ## Development ### Build & Run diff --git a/src/index.tsx b/src/index.tsx index f4f017b..f023b4f 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -4,13 +4,14 @@ import { render } from 'ink'; import App from './app.js'; import type { Screen } from './models/plan.js'; import { listPlans } from './services/persistence.js'; -import { loadModelPreference } from './services/copilot.js'; +import { loadModelPreference, ensureSkillsDirectory } from './services/copilot.js'; const args = process.argv.slice(2); const command = args[0] || 'home'; async function main(): Promise { await loadModelPreference(); + await ensureSkillsDirectory(); if (command === 'list') { const plans = await listPlans(); diff --git a/src/models/plan.ts b/src/models/plan.ts index d015e8a..80a2d4d 100644 --- a/src/models/plan.ts +++ b/src/models/plan.ts @@ -10,6 +10,11 @@ export interface Task { agentResult?: string; } +export interface SkillConfig { + name: string; + enabled: boolean; +} + export interface Plan { id: string; name: string; @@ -17,6 +22,7 @@ export interface Plan { createdAt: string; updatedAt: string; tasks: Task[]; + skills?: SkillConfig[]; } export interface ChatMessage { diff --git a/src/screens/breakdown.tsx b/src/screens/breakdown.tsx index aa2c0a5..0956b06 100644 --- a/src/screens/breakdown.tsx +++ b/src/screens/breakdown.tsx @@ -1,8 +1,9 @@ import React, { useState, useEffect } from 'react'; import { Box, Text, useInput } from 'ink'; -import type { Plan, Task, ChatMessage } from '../models/plan.js'; +import type { Plan, Task, ChatMessage, SkillConfig } from '../models/plan.js'; import { createPlan } from '../models/plan.js'; import { generateWBS } from '../services/planner.js'; +import { getSkillOptions, loadSkillConfigs } from '../services/copilot.js'; import { detectCycles, computeBatches } from '../utils/dependency-graph.js'; import TaskTree from '../components/task-tree.js'; import BatchView from '../components/batch-view.js'; @@ -37,6 +38,7 @@ export default function BreakdownScreen({ const [viewMode, setViewMode] = useState('tree'); const [attempt, setAttempt] = useState(0); const [streamText, setStreamText] = useState(''); + const [skillConfigs, setSkillConfigs] = useState([]); useEffect(() => { if (existingPlan) return; @@ -44,9 +46,22 @@ export default function BreakdownScreen({ setLoading(true); setError(null); setStreamText(''); - generateWBS(scopeDescription, (_delta, fullText) => { - setStreamText(fullText); - }, 2, codebaseContext || undefined) + + // Load skills and generate WBS + Promise.all([getSkillOptions(), loadSkillConfigs()]) + .then(([skillOptions, skills]) => { + setSkillConfigs(skills); + return generateWBS( + scopeDescription, + (_delta, fullText) => { + setStreamText(fullText); + }, + 2, + codebaseContext || undefined, + skillOptions, + skills + ); + }) .then((tasks) => { // Use first line only, strip markdown bold markers, and cap length const planName = scopeDescription @@ -59,6 +74,7 @@ export default function BreakdownScreen({ name: planName, description: scopeDescription, tasks, + skills: skillConfigs, }); setPlan(newPlan); setLoading(false); diff --git a/src/screens/refine.tsx b/src/screens/refine.tsx index 40bd325..57aeb71 100644 --- a/src/screens/refine.tsx +++ b/src/screens/refine.tsx @@ -1,9 +1,10 @@ import React, { useState, useCallback } from 'react'; import { Box, Text, useInput } from 'ink'; import TextInput from 'ink-text-input'; -import type { Plan, Task } from '../models/plan.js'; +import type { Plan, Task, SkillConfig } from '../models/plan.js'; import { refineWBS } from '../services/planner.js'; import { savePlan, summarizePlan } from '../services/persistence.js'; +import { getSkillOptions } from '../services/copilot.js'; import { detectCycles, computeBatches } from '../utils/dependency-graph.js'; import TaskTree from '../components/task-tree.js'; import BatchView from '../components/batch-view.js'; @@ -12,7 +13,7 @@ import Spinner from '../components/spinner.js'; import StreamingText from '../components/streaming-text.js'; import StatusBar from '../components/status-bar.js'; -type ViewMode = 'tree' | 'batch'; +type ViewMode = 'tree' | 'batch' | 'skills'; interface RefineScreenProps { plan: Plan; @@ -41,6 +42,19 @@ export default function RefineScreen({ const [editingTask, setEditingTask] = useState(null); const [commandMode, setCommandMode] = useState(false); + const toggleSkill = useCallback( + (skillName: string) => { + const skills = currentPlan.skills || []; + const updatedSkills = skills.map((s) => + s.name === skillName ? { ...s, enabled: !s.enabled } : s + ); + const updated = { ...currentPlan, skills: updatedSkills, updatedAt: new Date().toISOString() }; + setCurrentPlan(updated); + onPlanUpdated(updated); + }, + [currentPlan, onPlanUpdated] + ); + const moveTask = useCallback( (direction: 'up' | 'down') => { const tasks = [...currentPlan.tasks]; @@ -101,15 +115,28 @@ export default function RefineScreen({ } if (key.tab) { - setViewMode((v) => (v === 'tree' ? 'batch' : 'tree')); + setViewMode((v) => { + if (v === 'tree') return 'batch'; + if (v === 'batch') return 'skills'; + return 'tree'; + }); } else if (key.upArrow) { setSelectedIndex((i) => Math.max(0, i - 1)); } else if (key.downArrow) { - setSelectedIndex((i) => Math.min(currentPlan.tasks.length - 1, i + 1)); + if (viewMode === 'skills') { + const skills = currentPlan.skills || []; + setSelectedIndex((i) => Math.min(skills.length - 1, i + 1)); + } else { + setSelectedIndex((i) => Math.min(currentPlan.tasks.length - 1, i + 1)); + } } else if (ch === '[') { moveTask('up'); } else if (ch === ']') { moveTask('down'); + } else if (ch === ' ' && viewMode === 'skills') { + const skills = currentPlan.skills || []; + const skill = skills[selectedIndex]; + if (skill) toggleSkill(skill.name); } }); @@ -133,9 +160,12 @@ export default function RefineScreen({ setStreamText(''); setInput(''); - refineWBS(currentPlan.tasks, value, (_delta, fullText) => { - setStreamText(fullText); - }) + getSkillOptions() + .then((skillOptions) => + refineWBS(currentPlan.tasks, value, (_delta, fullText) => { + setStreamText(fullText); + }, skillOptions) + ) .then((tasks) => { const updated = { ...currentPlan, tasks, updatedAt: new Date().toISOString() }; setCurrentPlan(updated); @@ -173,6 +203,10 @@ export default function RefineScreen({ 📦 Batches + / + + 🎯 Skills + {cycles.length > 0 && ( @@ -190,8 +224,38 @@ export default function RefineScreen({ <> {viewMode === 'tree' ? ( - ) : ( + ) : viewMode === 'batch' ? ( + ) : ( + + + Active Skills + {(!currentPlan.skills || currentPlan.skills.length === 0) && ( + — no skills configured + )} + + {currentPlan.skills && currentPlan.skills.length > 0 ? ( + currentPlan.skills.map((skill, idx) => ( + + + {idx === selectedIndex ? '❯ ' : ' '} + + + {skill.enabled ? '✓' : '○'} {skill.name} + + + )) + ) : ( + + No custom skills found in .github/skills/ + + )} + + + Use ↑↓ to select, [space] to toggle, ⇥ to switch view + + + )} @@ -239,7 +303,10 @@ export default function RefineScreen({ ); diff --git a/src/services/copilot.test.ts b/src/services/copilot.test.ts new file mode 100644 index 0000000..3c29cbd --- /dev/null +++ b/src/services/copilot.test.ts @@ -0,0 +1,129 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { mkdir, writeFile, rm } from 'node:fs/promises'; +import { join } from 'node:path'; +import { existsSync } from 'node:fs'; +import { + ensureSkillsDirectory, + getSkillsDirectory, + listSkillFiles, + loadSkillConfigs, + getSkillOptions +} from './copilot.js'; + +const TEST_DIR = join(process.cwd(), '.planeteer-test'); +const TEST_SKILLS_DIR = join(TEST_DIR, 'skills'); + +describe('Skill Configuration', () => { + beforeEach(async () => { + // Clean up test directory + if (existsSync(TEST_DIR)) { + await rm(TEST_DIR, { recursive: true, force: true }); + } + }); + + afterEach(async () => { + // Clean up test directory + if (existsSync(TEST_DIR)) { + await rm(TEST_DIR, { recursive: true, force: true }); + } + }); + + describe('ensureSkillsDirectory', () => { + it('should create skills directory if it does not exist', async () => { + await ensureSkillsDirectory(); + const skillsDir = getSkillsDirectory(); + expect(existsSync(skillsDir)).toBe(true); + }); + + it('should not fail if skills directory already exists', async () => { + await ensureSkillsDirectory(); + await ensureSkillsDirectory(); // Should not throw + const skillsDir = getSkillsDirectory(); + expect(existsSync(skillsDir)).toBe(true); + }); + }); + + describe('listSkillFiles', () => { + it('should return empty array when no skill files exist', async () => { + await ensureSkillsDirectory(); + // Note: In test environment, example skills may already exist + const files = await listSkillFiles(); + expect(Array.isArray(files)).toBe(true); + // Files should only contain .yaml or .yml extensions + files.forEach(file => { + expect(file.endsWith('.yaml') || file.endsWith('.yml')).toBe(true); + }); + }); + + it('should list YAML skill files', async () => { + await ensureSkillsDirectory(); + const skillsDir = getSkillsDirectory(); + + await writeFile(join(skillsDir, 'skill1.yaml'), 'name: skill1\n'); + await writeFile(join(skillsDir, 'skill2.yml'), 'name: skill2\n'); + await writeFile(join(skillsDir, 'not-a-skill.txt'), 'ignore me\n'); + + const files = await listSkillFiles(); + expect(files).toContain('skill1.yaml'); + expect(files).toContain('skill2.yml'); + expect(files).not.toContain('not-a-skill.txt'); + }); + }); + + describe('loadSkillConfigs', () => { + it('should load skill configurations from existing files', async () => { + await ensureSkillsDirectory(); + const configs = await loadSkillConfigs(); + expect(Array.isArray(configs)).toBe(true); + // Should include the example-web-app-skill.yaml if it exists + if (configs.length > 0) { + expect(configs.every(c => c.enabled === true)).toBe(true); + expect(configs.every(c => typeof c.name === 'string')).toBe(true); + } + }); + + it('should load multiple skill configurations', async () => { + const configs = await loadSkillConfigs(); + expect(Array.isArray(configs)).toBe(true); + // All configs should have name and enabled properties + configs.forEach(config => { + expect(config).toHaveProperty('name'); + expect(config).toHaveProperty('enabled'); + expect(typeof config.name).toBe('string'); + expect(typeof config.enabled).toBe('boolean'); + }); + }); + + it('should gracefully handle malformed skill files', async () => { + const configs = await loadSkillConfigs(); + // Should successfully load at least the valid example skill + expect(Array.isArray(configs)).toBe(true); + }); + }); + + describe('getSkillOptions', () => { + it('should return skillDirectories when skills directory exists', async () => { + await ensureSkillsDirectory(); + const options = await getSkillOptions(); + + const skillFiles = await listSkillFiles(); + if (skillFiles.length > 0) { + expect(options).toHaveProperty('skillDirectories'); + expect(Array.isArray(options.skillDirectories)).toBe(true); + } else { + expect(options).toEqual({}); + } + }); + + it('should return skillDirectories path correctly', async () => { + await ensureSkillsDirectory(); + const skillsDir = getSkillsDirectory(); + const options = await getSkillOptions(); + + const skillFiles = await listSkillFiles(); + if (skillFiles.length > 0) { + expect(options.skillDirectories![0]).toBe(skillsDir); + } + }); + }); +}); diff --git a/src/services/copilot.ts b/src/services/copilot.ts index 0960e2a..f9e9a51 100644 --- a/src/services/copilot.ts +++ b/src/services/copilot.ts @@ -1,17 +1,24 @@ import { CopilotClient } from '@github/copilot-sdk'; +import { readFile, writeFile, mkdir, readdir } from 'node:fs/promises'; import type { SessionEvent } from '@github/copilot-sdk'; -import { readFile, writeFile, mkdir } from 'node:fs/promises'; import { join } from 'node:path'; import { existsSync } from 'node:fs'; -import type { ChatMessage } from '../models/plan.js'; +import type { ChatMessage, SkillConfig } from '../models/plan.js'; // Re-export SessionEvent for use in other modules export type { SessionEvent }; const SETTINGS_PATH = join(process.cwd(), '.planeteer', 'settings.json'); +const SKILLS_DIR = join(process.cwd(), '.github', 'skills'); interface Settings { model?: string; + disabledSkills?: string[]; +} + +export interface SkillOptions { + skillDirectories?: string[]; + disabledSkills?: string[]; } async function loadSettings(): Promise { @@ -30,6 +37,68 @@ async function saveSettings(settings: Settings): Promise { await writeFile(SETTINGS_PATH, JSON.stringify(settings, null, 2), 'utf-8'); } +/** Ensure the skills directory exists */ +export async function ensureSkillsDirectory(): Promise { + if (!existsSync(SKILLS_DIR)) { + await mkdir(SKILLS_DIR, { recursive: true }); + } +} + +/** Get the path to the skills directory */ +export function getSkillsDirectory(): string { + return SKILLS_DIR; +} + +/** List all skill files in the skills directory */ +export async function listSkillFiles(): Promise { + try { + if (!existsSync(SKILLS_DIR)) { + return []; + } + const files = await readdir(SKILLS_DIR); + return files.filter(f => f.endsWith('.yaml') || f.endsWith('.yml')); + } catch (err) { + console.error('Error listing skill files:', err); + return []; + } +} + +/** Load skill configurations from the skills directory */ +export async function loadSkillConfigs(): Promise { + const skillFiles = await listSkillFiles(); + const skills: SkillConfig[] = []; + + for (const file of skillFiles) { + try { + const content = await readFile(join(SKILLS_DIR, file), 'utf-8'); + // Parse YAML to extract skill name - simple parsing for name field + const nameMatch = content.match(/^name:\s*(.+)$/m); + if (nameMatch && nameMatch[1]) { + const name = nameMatch[1].trim(); + skills.push({ name, enabled: true }); + } + } catch (err) { + console.error(`Error loading skill file ${file}:`, err); + // Graceful degradation - skip malformed files + } + } + + return skills; +} + +/** Get skill options for Copilot SDK */ +export async function getSkillOptions(): Promise { + const skillFiles = await listSkillFiles(); + + if (skillFiles.length === 0) { + return {}; + } + + return { + skillDirectories: [SKILLS_DIR], + }; +} + export interface ModelEntry { id: string; label: string; @@ -119,6 +188,7 @@ export async function sendPrompt( systemPrompt: string, messages: ChatMessage[], callbacks: StreamCallbacks, + skillOptions?: SkillOptions, ): Promise { let copilot: CopilotClient; try { @@ -130,10 +200,27 @@ export async function sendPrompt( let session; try { - session = await copilot.createSession({ + interface SessionConfigWithSkills { + model: string; + streaming: boolean; + skillDirectories?: string[]; + disabledSkills?: string[]; + } + + const sessionConfig: SessionConfigWithSkills = { model: currentModel, streaming: true, - }); + }; + + if (skillOptions?.skillDirectories && skillOptions.skillDirectories.length > 0) { + sessionConfig.skillDirectories = skillOptions.skillDirectories; + } + + if (skillOptions?.disabledSkills && skillOptions.disabledSkills.length > 0) { + sessionConfig.disabledSkills = skillOptions.disabledSkills; + } + + session = await copilot.createSession(sessionConfig); } catch (err) { callbacks.onError(new Error(`Failed to create session: ${(err as Error).message}`)); return; @@ -192,10 +279,16 @@ export async function sendPrompt( export async function sendPromptSync( systemPrompt: string, messages: ChatMessage[], + options?: { timeoutMs?: number; onDelta?: (delta: string, fullText: string) => void; }, +): Promise { + const idleTimeoutMs = options?.timeoutMs ?? 120_000; + const onDelta = options?.onDelta; + const skillOptions = options?.skillOptions; options?: { timeoutMs?: number; onDelta?: (delta: string, fullText: string) => void; onSessionEvent?: (event: SessionEvent) => void; + skillOptions?: SkillOptions; }, ): Promise { const idleTimeoutMs = options?.timeoutMs ?? 120_000; @@ -256,7 +349,9 @@ export async function sendPromptSync( reject(err); } }, - onSessionEvent, + }, + skillOptions); + onSessionEvent, }); }); } diff --git a/src/services/planner.ts b/src/services/planner.ts index ef6bc6e..1804bed 100644 --- a/src/services/planner.ts +++ b/src/services/planner.ts @@ -1,5 +1,5 @@ -import type { ChatMessage, Task } from '../models/plan.js'; -import { sendPrompt, sendPromptSync, type StreamCallbacks } from './copilot.js'; +import type { ChatMessage, Task, SkillConfig } from '../models/plan.js'; +import { sendPrompt, sendPromptSync, type StreamCallbacks, type SkillOptions } from './copilot.js'; const CLARIFY_SYSTEM_PROMPT = `You are an expert project planner helping a user clarify the scope of their project. Ask focused clarifying questions to understand: @@ -100,11 +100,12 @@ export async function streamClarification( messages: ChatMessage[], callbacks: StreamCallbacks, codebaseContext?: string, + skillOptions?: SkillOptions, ): Promise { const systemPrompt = codebaseContext ? `${CLARIFY_SYSTEM_PROMPT}\n\n${codebaseContext}` : CLARIFY_SYSTEM_PROMPT; - return sendPrompt(systemPrompt, messages, callbacks); + return sendPrompt(systemPrompt, messages, callbacks, skillOptions); } /** Extract a JSON array from a response that may contain surrounding prose. */ @@ -131,18 +132,28 @@ export async function generateWBS( onDelta?: (delta: string, fullText: string) => void, maxRetries = 2, codebaseContext?: string, + skillOptions?: SkillOptions, + activeSkills?: SkillConfig[], ): Promise { let lastError: Error | null = null; - const userContent = codebaseContext + let userContent = codebaseContext ? `${scopeDescription}\n\n${codebaseContext}` : scopeDescription; + // Add skill context if active skills are provided + if (activeSkills && activeSkills.length > 0) { + const enabledSkills = activeSkills.filter((s) => s.enabled).map((s) => s.name); + if (enabledSkills.length > 0) { + userContent += `\n\nActive custom skills: ${enabledSkills.join(', ')}`; + } + } + for (let attempt = 0; attempt <= maxRetries; attempt++) { try { const result = await sendPromptSync(WBS_SYSTEM_PROMPT, [ { role: 'user', content: userContent }, - ], { onDelta }); + ], { onDelta, skillOptions }); const jsonStr = extractJsonArray(result); if (!jsonStr.startsWith('[')) { @@ -167,13 +178,14 @@ export async function refineWBS( currentTasks: Task[], refinementRequest: string, onDelta?: (delta: string, fullText: string) => void, + skillOptions?: SkillOptions, ): Promise { const result = await sendPromptSync(REFINE_SYSTEM_PROMPT, [ { role: 'user', content: `Current tasks:\n${JSON.stringify(currentTasks, null, 2)}\n\nRefinement request: ${refinementRequest}`, }, - ], { onDelta }); + ], { onDelta, skillOptions }); const jsonStr = extractJsonArray(result); const tasks = JSON.parse(jsonStr) as Task[];