From 3d050b706f2e8d75e28e149b1492ad50ebbaad47 Mon Sep 17 00:00:00 2001 From: Shahzaib Date: Mon, 16 Mar 2026 16:32:34 -0700 Subject: [PATCH] Feature Orchestrator Plugin: Core Structure (1/5) Add the portable agent plugin scaffold: - plugin.json with metadata, skill/agent/command registrations - .mcp.json with ADO MCP server configuration - hooks/state-utils.js for feature state management - hooks/hooks.json (empty, placeholder) - schemas/orchestrator-config.schema.json for config validation - README.md with plugin overview and installation instructions --- .../.github/plugin/plugin.json | 36 +++ feature-orchestrator-plugin/.mcp.json | 27 +++ feature-orchestrator-plugin/README.md | 166 ++++++++++++++ feature-orchestrator-plugin/hooks/hooks.json | 3 + .../hooks/state-utils.js | 210 ++++++++++++++++++ .../schemas/orchestrator-config.schema.json | 69 ++++++ 6 files changed, 511 insertions(+) create mode 100644 feature-orchestrator-plugin/.github/plugin/plugin.json create mode 100644 feature-orchestrator-plugin/.mcp.json create mode 100644 feature-orchestrator-plugin/README.md create mode 100644 feature-orchestrator-plugin/hooks/hooks.json create mode 100644 feature-orchestrator-plugin/hooks/state-utils.js create mode 100644 feature-orchestrator-plugin/schemas/orchestrator-config.schema.json diff --git a/feature-orchestrator-plugin/.github/plugin/plugin.json b/feature-orchestrator-plugin/.github/plugin/plugin.json new file mode 100644 index 00000000..5de3f5e7 --- /dev/null +++ b/feature-orchestrator-plugin/.github/plugin/plugin.json @@ -0,0 +1,36 @@ +{ + "name": "feature-orchestrator", + "description": "AI-driven feature development pipeline: Design → Plan → Backlog → Dispatch → Monitor. Orchestrates codebase research, design specs, PBI creation in ADO, dispatch to Copilot coding agent, and PR monitoring.", + "version": "1.0.0", + "author": { + "name": "Android Auth Team" + }, + "license": "MIT", + "keywords": [ + "feature-development", + "ai-orchestration", + "design-spec", + "pbi", + "copilot-agent", + "devops" + ], + "agents": [ + "../../agents" + ], + "commands": [ + "../../commands" + ], + "hooks": "../../hooks/hooks.json", + "mcpServers": "../../.mcp.json", + "skills": [ + "../../skills/codebase-researcher", + "../../skills/design-author", + "../../skills/design-reviewer", + "../../skills/feature-planner", + "../../skills/pbi-creator", + "../../skills/pbi-dispatcher-github", + "../../skills/pbi-dispatcher-ado", + "../../skills/pbi-dispatcher-ado-swe", + "../../skills/pr-validator" + ] +} diff --git a/feature-orchestrator-plugin/.mcp.json b/feature-orchestrator-plugin/.mcp.json new file mode 100644 index 00000000..d83a9308 --- /dev/null +++ b/feature-orchestrator-plugin/.mcp.json @@ -0,0 +1,27 @@ +{ + "inputs": [ + { + "id": "ado_org", + "type": "promptString", + "description": "Azure DevOps organization name ONLY — do NOT enter a URL (e.g., 'IdentityDivision' not 'https://dev.azure.com/IdentityDivision')", + "default": "YOUR_ORG" + } + ], + "servers": { + "ado": { + "type": "stdio", + "command": "npx", + "args": [ + "-y", + "@azure-devops/mcp", + "${input:ado_org}", + "-d", + "core", + "work", + "work-items", + "repositories", + "pipelines" + ] + } + } +} diff --git a/feature-orchestrator-plugin/README.md b/feature-orchestrator-plugin/README.md new file mode 100644 index 00000000..d72dde02 --- /dev/null +++ b/feature-orchestrator-plugin/README.md @@ -0,0 +1,166 @@ +# Feature Orchestrator Plugin + +AI-driven feature development pipeline for GitHub Copilot. Automates the full lifecycle: + +**Design → Plan → Backlog → Dispatch → Monitor** + +1. **Design** — Research the codebase, write a design spec with solution options +2. **Plan** — Decompose the approved design into right-sized, repo-targeted work items +3. **Backlog** — Create work items in Azure DevOps with proper dependencies +4. **Dispatch** — Send work items to GitHub Copilot coding agent for implementation +5. **Monitor** — Track agent PRs and iterate on feedback + +## Installation + +### From VS Code + +1. Open the Extensions sidebar (`Ctrl+Shift+X`) +2. Search for `@agentPlugins` and browse available plugins +3. Install **feature-orchestrator** + +### Local Installation (for development) + +```jsonc +// In your VS Code settings.json: +"chat.plugins.paths": { + "/path/to/feature-orchestrator-plugin": true +} +``` + +## Setup + +After installing, configure the plugin for your project: + +1. Open a chat and run: `/feature-orchestrator-plugin:setup` +2. The setup wizard guides you through: + - **Project info** — name and description + - **Repository mapping** — which modules map to which GitHub repos + - **Azure DevOps** — organization, project, work item type + - **Design docs** — where to store design specs + - **Prerequisites** — checks for `gh` CLI, `node`, authentication + +This creates `.github/orchestrator-config.json` in your workspace. Commit it to share with your team. + +## Quick Start + +After setup, describe a feature to start the pipeline: + +``` +/feature-orchestrator-plugin:feature-design I want to add retry logic with exponential backoff to the API client +``` + +Or use the agent directly: +``` +@feature-orchestrator-plugin:feature-orchestrator.agent Add push notification support for auth state changes +``` + +## Commands + +| Command | Description | +|---------|-------------| +| `/setup` | Configure the plugin for this project | +| `/feature-design` | Start a new feature — research + design spec | +| `/feature-plan` | Decompose approved design into work items | +| `/feature-backlog` | Create work items in Azure DevOps | +| `/feature-dispatch` | Send work items to Copilot coding agent | +| `/feature-status` | Check agent PR status | +| `/feature-continue` | Resume a feature from its current step | +| `/feature-pr-iterate` | Review and iterate on agent PRs | + +(All commands are prefixed with `feature-orchestrator-plugin:` in the UI) + +## Skills + +| Skill | Description | +|-------|-------------| +| `codebase-researcher` | Systematic codebase exploration with evidence-based findings | +| `design-author` | Write detailed design specs with solution options and trade-offs | +| `design-reviewer` | Address inline review comments on design specs | +| `feature-planner` | Decompose features into self-contained, right-sized work items | +| `pbi-creator` | Create and link work items in Azure DevOps | +| `pbi-dispatcher` | Dispatch work items to Copilot coding agent | + +## Configuration + +The plugin uses `.github/orchestrator-config.json` for project-specific settings: + +```jsonc +{ + "project": { + "name": "My Project", + "description": "Brief description" + }, + "repositories": { + "core-repo": { + "slug": "my-org/core-repo", + "host": "github", + "baseBranch": "main" + }, + "api-repo": { + "slug": "my-org/api-repo", + "host": "github", + "baseBranch": "dev", + "accountType": "emu" + } + }, + "modules": { + "core": { "repo": "core-repo", "path": "core/", "purpose": "Shared utilities and data models" }, + "api": { "repo": "core-repo", "path": "api/", "purpose": "Public API surface" }, + "service": { "repo": "api-repo", "purpose": "Backend service" } + }, + "ado": { + "org": "my-org", + "project": "Engineering", + "workItemType": "Product Backlog Item", + "iterationDepth": 6 + }, + "design": { + "docsPath": "docs/designs/", + "templatePath": null + } +} +``` + +### Per-Developer Config + +GitHub account mappings are stored per-developer (gitignored): + +`.github/developer-local.json`: +```json +{ + "github_accounts": { + "public": "your-github-username", + "emu": "your-emu-username" + } +} +``` + +## Prerequisites + +- **VS Code** 1.109+ with GitHub Copilot +- **GitHub CLI** (`gh`) — for dispatching and PR monitoring +- **Node.js** — for state management (`state-utils.js` installed to `~/.feature-orchestrator/`) +- **Azure DevOps MCP Server** — for work item management (optional but recommended) + +## Architecture + +``` +Plugin +├── agents/ # Orchestrator agent (conductor) +├── commands/ # Slash commands with agent routing +├── skills/ # Specialized skills for each phase +│ ├── codebase-researcher/ +│ ├── design-author/ +│ ├── design-reviewer/ +│ ├── feature-planner/ +│ │ └── references/pbi-template.md +│ ├── pbi-creator/ +│ └── pbi-dispatcher/ +├── hooks/ # State management CLI +├── schemas/ # Config JSON schema +└── .mcp.json # MCP server configuration +``` + +## License + +MIT diff --git a/feature-orchestrator-plugin/hooks/hooks.json b/feature-orchestrator-plugin/hooks/hooks.json new file mode 100644 index 00000000..deffac97 --- /dev/null +++ b/feature-orchestrator-plugin/hooks/hooks.json @@ -0,0 +1,3 @@ +{ + "hooks": {} +} diff --git a/feature-orchestrator-plugin/hooks/state-utils.js b/feature-orchestrator-plugin/hooks/state-utils.js new file mode 100644 index 00000000..007d748c --- /dev/null +++ b/feature-orchestrator-plugin/hooks/state-utils.js @@ -0,0 +1,210 @@ +#!/usr/bin/env node +/** + * Feature Orchestrator — State Management CLI + * + * Manages feature pipeline state for the orchestrator dashboard. + * State is stored at ~/.feature-orchestrator/state.json (fixed location). + * This script is installed to ~/.feature-orchestrator/state-utils.js during setup. + * + * Usage: + * node ~/.feature-orchestrator/state-utils.js add-feature '{"name": "...", "step": "designing"}' + * node ~/.feature-orchestrator/state-utils.js set-step "" + * node ~/.feature-orchestrator/state-utils.js set-design "" '{"docPath":"...","status":"approved"}' + * node ~/.feature-orchestrator/state-utils.js add-pbi "" '{"adoId":123,"title":"...","module":"...","status":"Committed"}' + * node ~/.feature-orchestrator/state-utils.js add-agent-pr "" '{"repo":"...","prNumber":1,"prUrl":"...","status":"open"}' + * node ~/.feature-orchestrator/state-utils.js list-features + * node ~/.feature-orchestrator/state-utils.js get-feature "" + * node ~/.feature-orchestrator/state-utils.js get + */ + +const fs = require('fs'); +const path = require('path'); +const os = require('os'); + +// Fixed state directory — always ~/.feature-orchestrator/ +const STATE_DIR = path.join(os.homedir(), '.feature-orchestrator'); +const STATE_FILE = path.join(STATE_DIR, 'state.json'); + +function ensureDir() { + if (!fs.existsSync(STATE_DIR)) fs.mkdirSync(STATE_DIR, { recursive: true }); +} + +function readState() { + if (!fs.existsSync(STATE_FILE)) return { version: 1, features: [], lastUpdated: 0 }; + try { return JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8')); } + catch { return { version: 1, features: [], lastUpdated: 0 }; } +} + +function writeState(state) { + ensureDir(); + state.lastUpdated = Date.now(); + fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2), 'utf-8'); +} + +function findFeature(state, identifier) { + if (!identifier) return null; + const byId = state.features.find(f => f.id === identifier); + if (byId) return byId; + const lower = identifier.toLowerCase(); + return state.features.find(f => f.name && f.name.toLowerCase() === lower) + || state.features.find(f => f.name && f.name.toLowerCase().includes(lower)) + || null; +} + +function checkAutoCompletion(feature) { + if (!feature.artifacts) return; + const pbis = feature.artifacts.pbis || []; + const prs = feature.artifacts.agentPrs || []; + if (pbis.length === 0) return; + const allPbisResolved = pbis.every(p => + ['Resolved', 'Done', 'Closed', 'Removed'].includes(p.status)); + const allPrsClosed = prs.length > 0 && prs.every(p => + ['merged', 'closed'].includes(p.status)); + if (allPbisResolved && allPrsClosed && feature.step !== 'completed') { + feature.step = 'completed'; + feature.completedAt = Date.now(); + if (!feature.phaseTimestamps) feature.phaseTimestamps = {}; + feature.phaseTimestamps.completed = Date.now(); + } +} + +const [,, command, ...args] = process.argv; + +switch (command) { + case 'get': { + const state = readState(); + console.log(JSON.stringify(state, null, 2)); + break; + } + case 'list-features': { + const state = readState(); + console.log(JSON.stringify(state.features.map(f => ({ + name: f.name, step: f.step, id: f.id, + updatedAt: new Date(f.updatedAt).toISOString() + })), null, 2)); + break; + } + case 'get-feature': { + const state = readState(); + const feature = findFeature(state, args[0]); + console.log(JSON.stringify(feature || null, null, 2)); + break; + } + case 'add-feature': { + const state = readState(); + const feature = JSON.parse(args[0]); + if (!feature.id) { + feature.id = 'feature-' + Date.now() + '-' + Math.random().toString(36).slice(2, 6); + } + const idx = state.features.findIndex(f => + f.name && feature.name && f.name.toLowerCase() === feature.name.toLowerCase()); + if (idx >= 0) { + state.features[idx] = { ...state.features[idx], ...feature, updatedAt: Date.now() }; + } else { + state.features.push({ + ...feature, + startedAt: Date.now(), + updatedAt: Date.now(), + artifacts: { designSpec: null, pbis: [], agentPrs: [] }, + phaseTimestamps: { [feature.step || 'designing']: Date.now() } + }); + } + writeState(state); + console.log(JSON.stringify({ ok: true, id: feature.id })); + break; + } + case 'set-step': { + const state = readState(); + const feature = findFeature(state, args[0]); + if (feature) { + feature.step = args[1]; + feature.updatedAt = Date.now(); + if (!feature.phaseTimestamps) feature.phaseTimestamps = {}; + feature.phaseTimestamps[args[1]] = Date.now(); + writeState(state); + console.log(JSON.stringify({ ok: true })); + } else { + console.log(JSON.stringify({ ok: false, error: 'Feature not found: ' + args[0] })); + } + break; + } + case 'set-design': { + const state = readState(); + const feature = findFeature(state, args[0]); + if (feature) { + const design = JSON.parse(args[1]); + if (!feature.artifacts) feature.artifacts = { designSpec: null, pbis: [], agentPrs: [] }; + feature.artifacts.designSpec = { ...feature.artifacts.designSpec, ...design }; + feature.updatedAt = Date.now(); + writeState(state); + console.log(JSON.stringify({ ok: true })); + } else { + console.log(JSON.stringify({ ok: false, error: 'Feature not found' })); + } + break; + } + case 'add-pbi': { + const state = readState(); + const feature = findFeature(state, args[0]); + if (feature) { + const pbi = JSON.parse(args[1]); + if (!feature.artifacts) feature.artifacts = { designSpec: null, pbis: [], agentPrs: [] }; + if (!feature.artifacts.pbis) feature.artifacts.pbis = []; + const existingIdx = feature.artifacts.pbis.findIndex(p => p.adoId === pbi.adoId); + if (existingIdx >= 0) { + feature.artifacts.pbis[existingIdx] = { ...feature.artifacts.pbis[existingIdx], ...pbi }; + } else { + feature.artifacts.pbis.push(pbi); + } + feature.updatedAt = Date.now(); + checkAutoCompletion(feature); + writeState(state); + console.log(JSON.stringify({ ok: true })); + } else { + console.log(JSON.stringify({ ok: false, error: 'Feature not found' })); + } + break; + } + case 'add-agent-pr': { + const state = readState(); + const feature = findFeature(state, args[0]); + if (feature) { + const pr = JSON.parse(args[1]); + if (!feature.artifacts) feature.artifacts = { designSpec: null, pbis: [], agentPrs: [] }; + if (!feature.artifacts.agentPrs) feature.artifacts.agentPrs = []; + const existingIdx = feature.artifacts.agentPrs.findIndex(p => + p.prNumber === pr.prNumber && p.repo === pr.repo); + if (existingIdx >= 0) { + feature.artifacts.agentPrs[existingIdx] = { ...feature.artifacts.agentPrs[existingIdx], ...pr }; + } else { + feature.artifacts.agentPrs.push(pr); + } + feature.updatedAt = Date.now(); + checkAutoCompletion(feature); + writeState(state); + console.log(JSON.stringify({ ok: true })); + } else { + console.log(JSON.stringify({ ok: false, error: 'Feature not found' })); + } + break; + } + case 'set-agent-info': { + const state = readState(); + const feature = findFeature(state, args[0]); + if (feature) { + const info = JSON.parse(args[1]); + feature.agentInfo = { ...feature.agentInfo, ...info }; + feature.updatedAt = Date.now(); + writeState(state); + console.log(JSON.stringify({ ok: true })); + } else { + console.log(JSON.stringify({ ok: false, error: 'Feature not found' })); + } + break; + } + default: + console.error('Feature Orchestrator State CLI'); + console.error('Commands: get, list-features, get-feature, add-feature, set-step,'); + console.error(' set-design, add-pbi, add-agent-pr, set-agent-info'); + process.exit(1); +} diff --git a/feature-orchestrator-plugin/schemas/orchestrator-config.schema.json b/feature-orchestrator-plugin/schemas/orchestrator-config.schema.json new file mode 100644 index 00000000..a31255e3 --- /dev/null +++ b/feature-orchestrator-plugin/schemas/orchestrator-config.schema.json @@ -0,0 +1,69 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "Configuration for the Feature Orchestrator plugin. Run /feature-orchestrator-plugin:setup to generate this file interactively.", + "type": "object", + "properties": { + "project": { + "type": "object", + "description": "Project metadata", + "properties": { + "name": { "type": "string", "description": "Short project name (e.g., 'Android Auth')" }, + "description": { "type": "string", "description": "One-line project description" } + } + }, + "repositories": { + "type": "object", + "description": "Repository mapping. Keys are short names, values describe the hosting details.", + "additionalProperties": { + "type": "object", + "properties": { + "slug": { "type": "string", "description": "GitHub org/repo or ADO org/project/repo" }, + "host": { "type": "string", "enum": ["github", "ado"], "default": "github", "description": "Where the repo is hosted" }, + "baseBranch": { "type": "string", "default": "main", "description": "Default base branch for PRs" }, + "accountType": { "type": "string", "default": "public", "description": "Account label for gh CLI switching (maps to a username in developer-local.json). Optional if only one GitHub account." } + }, + "required": ["slug"] + } + }, + "modules": { + "type": "object", + "description": "Module-to-repo mapping. Keys are module names referenced in work items. Each module maps to a repository.", + "additionalProperties": { + "type": "object", + "properties": { + "repo": { "type": "string", "description": "Key from the repositories map that this module belongs to" }, + "path": { "type": "string", "description": "Path within the repo (e.g., 'common/' or 'src/api/'). Optional — omit if module is the whole repo." }, + "purpose": { "type": "string", "description": "Brief description of what this module does (used by codebase-researcher)" } + }, + "required": ["repo"] + } + }, + "github": { + "type": "object", + "description": "GitHub configuration. Usernames are stored in developer-local.json (gitignored), NOT here.", + "properties": { + "configFile": { "type": "string", "default": ".github/developer-local.json", "description": "Path to per-developer config file (gitignored) that maps repos to GitHub usernames" } + } + }, + "ado": { + "type": "object", + "description": "Azure DevOps configuration", + "properties": { + "org": { "type": "string", "description": "ADO organization URL or name" }, + "project": { "type": "string", "description": "ADO project name (e.g., 'Engineering')" }, + "workItemType": { "type": "string", "default": "Product Backlog Item", "description": "Work item type for PBIs" }, + "iterationDepth": { "type": "integer", "default": 6, "description": "Depth for iteration discovery (6 for monthly sprints)" } + } + }, + "design": { + "type": "object", + "description": "Design spec configuration", + "properties": { + "docsPath": { "type": "string", "description": "Path to design docs folder (e.g., 'design-docs/' or 'docs/designs/')" }, + "templatePath": { "type": "string", "description": "Path to design spec template (optional)" }, + "folderPattern": { "type": "string", "default": "[{platform}] {featureName}", "description": "Folder naming pattern for new designs" }, + "reviewRepo": { "type": "string", "description": "ADO/GitHub repo for design review PRs (optional)" } + } + } + } +}