Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions .github/workflows/sync-shared-skills.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: sync-shared-skills

on:
pull_request:
branches:
- main
paths:
- "plugins/**"
- "shared/skills/**"

permissions:
contents: write

jobs:
sync-shared-skills:
name: sync-shared-skills
runs-on: ubuntu-latest
steps:
- name: generate app token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: 3189942
private-key: ${{ secrets.POWER_PLATFORM_SKILLS_APP_PRIVATE_KEY }}

Comment thread
priyanshu92 marked this conversation as resolved.
Outdated
- name: checkout
uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}
token: ${{ steps.app-token.outputs.token }}

Comment thread
priyanshu92 marked this conversation as resolved.
Outdated
- name: setup-node
uses: actions/setup-node@v4
with:
node-version: 20

- name: sync shared skills to all plugins
run: node scripts/sync-shared-skills.js

Comment thread
priyanshu92 marked this conversation as resolved.
Outdated
- name: commit and push if changed
run: |
git diff --quiet && exit 0
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add "plugins/*/skills/*/SKILL.md"
git commit -m "Auto-sync shared skill wrappers to new plugins"
git push
21 changes: 21 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ power-platform-skills/
│ ├── commands/ # Command entry points
│ ├── shared/ # Shared resources and documentation
│ └── skills/ # Skill workflows (SKILL.md in subdirectories)
├── scripts/ # Repo-level utility scripts
│ └── sync-shared-skills.js # Auto-generates SKILL.md wrappers in all plugins
├── shared/ # Cross-plugin shared resources
│ └── skills/ # Shared skill definitions
│ └── <skill-name>/ # SKILL.template.md + workflow .md files
├── AGENTS.md # Generic development guidelines (this file)
└── README.md # Repository overview
```
Expand Down Expand Up @@ -48,6 +53,22 @@ Each plugin follows this structure:

Skills are defined in `SKILL.md` files with YAML frontmatter (name, description, allowed-tools, model, hooks). The `allowed-tools` field must use a **comma-separated list** (e.g., `allowed-tools: Read, Write, Edit, Bash, Glob, Grep`) — not JSON array syntax (`["Read", "Write"]`) or YAML list syntax. Each skill may include validation scripts in a `scripts/` subdirectory, run as Stop hooks when the skill session ends.

## Cross-Plugin Shared Skills

Skills that apply to all plugins live in `shared/skills/<skill-name>/`. The workflow logic is written once in a shared `.md` file, and each plugin has a thin `skills/<skill-name>/SKILL.md` that contains only the YAML frontmatter and a reference to the shared workflow file.

**Pattern:**
- `shared/skills/<skill-name>/<workflow>.md` — Full workflow (phases, instructions, field definitions)
- `shared/skills/<skill-name>/SKILL.template.md` — Template SKILL.md (frontmatter + reference to workflow); supports `{{PLUGIN_NAME}}` placeholder
- `shared/skills/<skill-name>/config.json` — Controls which plugins get this skill:
- `{ "plugins": "*" }` — all plugins (default if no config.json)
- `{ "plugins": ["power-pages", "code-apps"] }` — only listed plugins
- `plugins/<plugin>/skills/<skill-name>/SKILL.md` — Auto-generated from the template above

This keeps the skill discoverable in each plugin while avoiding content duplication. When updating a shared skill, edit the workflow file and/or `SKILL.template.md` in `shared/` — the per-plugin wrappers are auto-generated.

**Auto-sync:** Run `node scripts/sync-shared-skills.js` locally to auto-generate missing SKILL.md wrappers in all plugins. A CI workflow runs this on PRs that touch `plugins/` or `shared/skills/` and commits any generated files back to the PR automatically.
Comment thread
priyanshu92 marked this conversation as resolved.
Outdated

## Code Conventions

**DRY (Don't Repeat Yourself):** Never duplicate logic across files. Each plugin has shared utilities (e.g., `scripts/lib/`) and shared reference docs (e.g., `references/`). Always check for and reuse existing helpers before writing new code. When adding shared logic, put it in the plugin's shared modules — not in individual skill directories.
Expand Down
13 changes: 13 additions & 0 deletions plugins/code-apps/skills/report-issue/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
name: report-issue
description: >
Use this skill when the user wants to "report a bug", "file an issue",
"report an issue", "submit a bug report", or report any problem
with the code-apps plugin to the GitHub repository.
user-invocable: true
argument-hint: "[optional: brief description of the bug]"
allowed-tools: Read, Bash, Glob, Grep, AskUserQuestion, TaskCreate, TaskUpdate, TaskList
model: sonnet
---

**Shared workflow: [report-issue-workflow.md](${CLAUDE_PLUGIN_ROOT}/../../shared/skills/report-issue/report-issue-workflow.md)** — Read and follow all phases defined in that file.
13 changes: 13 additions & 0 deletions plugins/model-apps/skills/report-issue/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
name: report-issue
description: >
Use this skill when the user wants to "report a bug", "file an issue",
"report an issue", "submit a bug report", or report any problem
with the model-apps plugin to the GitHub repository.
user-invocable: true
argument-hint: "[optional: brief description of the bug]"
allowed-tools: Read, Bash, Glob, Grep, AskUserQuestion, TaskCreate, TaskUpdate, TaskList
model: sonnet
---

**Shared workflow: [report-issue-workflow.md](${CLAUDE_PLUGIN_ROOT}/../../shared/skills/report-issue/report-issue-workflow.md)** — Read and follow all phases defined in that file.
13 changes: 13 additions & 0 deletions plugins/power-pages/skills/report-issue/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
name: report-issue
description: >
Use this skill when the user wants to "report a bug", "file an issue",
"report an issue", "submit a bug report", or report any problem
with the power-pages plugin to the GitHub repository.
user-invocable: true
argument-hint: "[optional: brief description of the bug]"
allowed-tools: Read, Bash, Glob, Grep, AskUserQuestion, TaskCreate, TaskUpdate, TaskList
model: sonnet
---

**Shared workflow: [report-issue-workflow.md](${CLAUDE_PLUGIN_ROOT}/../../shared/skills/report-issue/report-issue-workflow.md)** — Read and follow all phases defined in that file.
138 changes: 138 additions & 0 deletions scripts/sync-shared-skills.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
#!/usr/bin/env node

/**
* Syncs shared skills to targeted plugins.
*
* Each shared skill in shared/skills/<skill-name>/ has a config.json that
* specifies which plugins it applies to:
* { "plugins": "*" } — all plugins (default if no config)
* { "plugins": ["power-pages", "code-apps"] } — only listed plugins
*
* For each targeted plugin, copies SKILL.template.md → plugins/<plugin>/skills/<skill-name>/SKILL.md
* with {{PLUGIN_NAME}} replaced by the plugin's name from plugin.json.
*
* Modes:
* --check Report missing wrappers and exit 1 if any are found (CI mode).
* (default) Generate missing wrappers in place.
*
* Usage:
* node scripts/sync-shared-skills.js # auto-generate missing wrappers
* node scripts/sync-shared-skills.js --check # CI: fail if any are missing
*/

const fs = require("fs");
const path = require("path");

const ROOT = path.resolve(__dirname, "..");
const PLUGINS_DIR = path.join(ROOT, "plugins");
const SHARED_SKILLS_DIR = path.join(ROOT, "shared", "skills");

const checkOnly = process.argv.includes("--check");

function getDirectories(dir) {
if (!fs.existsSync(dir)) return [];
return fs
.readdirSync(dir, { withFileTypes: true })
.filter((d) => d.isDirectory())
.map((d) => d.name);
}

// Read plugin name from plugin.json
function getPluginName(plugin) {
const pluginJsonPath = path.join(PLUGINS_DIR, plugin, ".claude-plugin", "plugin.json");
if (fs.existsSync(pluginJsonPath)) {
try {
const pluginJson = JSON.parse(fs.readFileSync(pluginJsonPath, "utf-8"));
return pluginJson.name || plugin;
} catch {
return plugin;
}
}
return plugin;
}

// Read config.json for a shared skill to determine target plugins
function getTargetPlugins(skill, allPlugins) {
const configPath = path.join(SHARED_SKILLS_DIR, skill, "config.json");
if (fs.existsSync(configPath)) {
try {
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
if (config.plugins === "*") {
return allPlugins;
}
if (Array.isArray(config.plugins)) {
// Match by plugin directory name or plugin.json name
return allPlugins.filter((plugin) => {
const pluginName = getPluginName(plugin);
return config.plugins.includes(plugin) || config.plugins.includes(pluginName);
});
}
} catch {
// Fall through to default
}
}
// Default: all plugins
return allPlugins;
}

const sharedSkills = getDirectories(SHARED_SKILLS_DIR);
if (sharedSkills.length === 0) {
console.log("No shared skills found in shared/skills/. Nothing to sync.");
process.exit(0);
}

const allPlugins = getDirectories(PLUGINS_DIR);
if (allPlugins.length === 0) {
console.log("No plugins found in plugins/. Nothing to sync.");
process.exit(0);
}

const missing = [];

for (const skill of sharedSkills) {
const targetPlugins = getTargetPlugins(skill, allPlugins);
for (const plugin of targetPlugins) {
const targetPath = path.join(PLUGINS_DIR, plugin, "skills", skill, "SKILL.md");
if (!fs.existsSync(targetPath)) {
missing.push({ plugin, skill, targetPath });
Comment thread
priyanshu92 marked this conversation as resolved.
Outdated
}
}
Comment thread
priyanshu92 marked this conversation as resolved.
Outdated
}

if (missing.length === 0) {
console.log(
`All plugins are in sync for all ${sharedSkills.length} shared skill(s).`
);
process.exit(0);
}

if (checkOnly) {
console.error("Shared skill sync check failed!\n");
console.error("The following plugins are missing shared skill wrappers:\n");
for (const { plugin, skill } of missing) {
console.error(` - Plugin "${plugin}" is missing shared skill "${skill}"`);
}
console.error("\nRun 'node scripts/sync-shared-skills.js' to auto-generate them.");
process.exit(1);
}

// Generate missing wrappers
for (const { plugin, skill, targetPath } of missing) {
const templatePath = path.join(SHARED_SKILLS_DIR, skill, "SKILL.template.md");
if (!fs.existsSync(templatePath)) {
console.error(
` SKIP: No SKILL.template.md found for shared skill "${skill}" — expected at shared/skills/${skill}/SKILL.template.md`
);
continue;
}

const pluginName = getPluginName(plugin);
const template = fs.readFileSync(templatePath, "utf-8");
const content = template.replace(/\{\{PLUGIN_NAME\}\}/g, pluginName);
const targetDir = path.dirname(targetPath);
fs.mkdirSync(targetDir, { recursive: true });
fs.writeFileSync(targetPath, content, "utf-8");
console.log(` CREATED: plugins/${plugin}/skills/${skill}/SKILL.md (plugin: ${pluginName})`);
}

console.log(`\nSynced ${missing.length} shared skill wrapper(s).`);
13 changes: 13 additions & 0 deletions shared/skills/report-issue/SKILL.template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
name: report-issue
description: >
Use this skill when the user wants to "report a bug", "file an issue",
"report an issue", "submit a bug report", or report any problem
with the {{PLUGIN_NAME}} plugin to the GitHub repository.
user-invocable: true
argument-hint: "[optional: brief description of the bug]"
allowed-tools: Read, Bash, Glob, Grep, AskUserQuestion, TaskCreate, TaskUpdate, TaskList
model: sonnet
---

**Shared workflow: [report-issue-workflow.md](${CLAUDE_PLUGIN_ROOT}/../../shared/skills/report-issue/report-issue-workflow.md)** — Read and follow all phases defined in that file.
3 changes: 3 additions & 0 deletions shared/skills/report-issue/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"plugins": "*"
}
Loading
Loading