Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
32 changes: 23 additions & 9 deletions .github/workflows/sync.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@ name: Sync to consumers
on:
push:
branches: [main]
paths: ['lib/**']
paths: ['lib/**', 'templates/**', 'scripts/generate-claudemd.js']

permissions:
contents: read

jobs:
sync:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
repo: [agentsys, next-task, ship, enhance, deslop, learn, consult, debate, drift-detect, repo-map, sync-docs, audit-project, perf]
repo: [agentsys, next-task, ship, enhance, deslop, learn, consult, debate, drift-detect, repo-map, sync-docs, audit-project, perf, web-ctl]
steps:
- name: Checkout agent-core
uses: actions/checkout@v4
Expand All @@ -29,27 +33,37 @@ jobs:
rm -rf target/lib/
cp -r source/lib/ target/lib/

- name: Generate CLAUDE.md
run: node source/scripts/generate-claudemd.js --target target --template source/templates/CLAUDE.md.tmpl

- name: Check for changes
id: diff
working-directory: target
run: |
git diff --quiet && echo "changed=false" >> $GITHUB_OUTPUT || echo "changed=true" >> $GITHUB_OUTPUT
if [ -n "$(git status --porcelain)" ]; then
echo "changed=true" >> $GITHUB_OUTPUT
else
echo "changed=false" >> $GITHUB_OUTPUT
fi

- name: Create PR
if: steps.diff.outputs.changed == 'true'
working-directory: target
env:
GH_TOKEN: ${{ secrets.SYNC_TOKEN }}
TARGET_REPO: agent-sh/${{ matrix.repo }}
REPO_NAME: ${{ matrix.repo }}
run: |
BRANCH="chore/sync-core-$(date +%Y%m%d-%H%M%S)"
BRANCH="chore/sync-core-${REPO_NAME}-$(date +%Y%m%d-%H%M%S)"
git config user.name "agent-core-bot"
git config user.email "noreply@agent-sh.github.io"
git checkout -b "$BRANCH"
git add lib/
git commit -m "chore: sync core lib from agent-core"
git add lib/ CLAUDE.md
git commit -m "chore: sync core lib and CLAUDE.md from agent-core"
git push origin "$BRANCH"
gh pr create \
--repo agent-sh/${{ matrix.repo }} \
--title "chore: sync core lib from agent-core" \
--body "Automated sync of lib/ from [agent-core](https://github.com/agent-sh/agent-core)." \
--repo "$TARGET_REPO" \
--base main \
--title "chore: sync core lib and CLAUDE.md from agent-core" \
--body "Automated sync of lib/ and CLAUDE.md from [agent-core](https://github.com/agent-sh/agent-core)." \
--head "$BRANCH"
36 changes: 31 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,45 @@ Shared core libraries for all agent-sh plugins. Changes here are automatically s

## Consumers

| Repo | How it receives lib/ |
|------|---------------------|
| Repo | How it receives lib/ and CLAUDE.md |
|------|------------------------------------|
| agentsys | PR → merge → `sync-lib` propagates to 13 bundled plugins |
| agnix | PR → merge (plugin uses lib/ directly) |
| next-task | PR → merge (plugin uses lib/ directly) |
| ship | PR → merge (plugin uses lib/ directly) |
| enhance | PR → merge (plugin uses lib/ directly) |
| deslop | PR → merge (plugin uses lib/ directly) |
| learn | PR → merge (plugin uses lib/ directly) |
| consult | PR → merge (plugin uses lib/ directly) |
| debate | PR → merge (plugin uses lib/ directly) |
| drift-detect | PR → merge (plugin uses lib/ directly) |
| repo-map | PR → merge (plugin uses lib/ directly) |
| sync-docs | PR → merge (plugin uses lib/ directly) |
| audit-project | PR → merge (plugin uses lib/ directly) |
| perf | PR → merge (plugin uses lib/ directly) |
| web-ctl | PR → merge (plugin uses lib/ directly) |

## How sync works

On merge to `main`, the `sync-core` workflow opens PRs in all consumer repos with the updated `lib/` directory. Consumer repos review and merge at their own pace.
On merge to `main`, the `sync` workflow opens PRs in all consumer repos with the updated `lib/` directory and a freshly generated `CLAUDE.md` (rendered from `templates/CLAUDE.md.tmpl`). Consumer repos review and merge at their own pace.

## CLAUDE.md generation

Each consumer repo receives a generated `CLAUDE.md` rendered from `templates/CLAUDE.md.tmpl`. The generator reads `package.json` and optionally `components.json` from the target repo.

Available template variables:
- `{{pluginName}}` - package name with `@agentsys/` prefix stripped
- `{{description}}` - package.json description
- `{{#agents}}` / `{{#skills}}` / `{{#commands}}` - conditional sections from components.json

To test generation locally:

```bash
node scripts/generate-claudemd.js --target ../some-plugin --template templates/CLAUDE.md.tmpl
```

## Developing

Edit files in `lib/`. On merge, changes propagate automatically. To test locally before merging:
Edit files in `lib/` for library changes. Edit `templates/CLAUDE.md.tmpl` to change the CLAUDE.md generated for all consumer plugins. On merge, changes propagate automatically. To test locally before merging:

```bash
# Copy to a consumer repo for testing
Expand Down
116 changes: 116 additions & 0 deletions scripts/generate-claudemd.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
#!/usr/bin/env node
'use strict';

const fs = require('node:fs');
const path = require('node:path');

function parseArgs(argv) {
const args = {};
for (let i = 2; i < argv.length; i += 2) {
if (!argv[i].startsWith('--')) {
console.error(`[ERROR] Expected flag starting with --, got: ${argv[i]}`);
process.exit(1);
}
const key = argv[i].replace(/^--/, '');
if (i + 1 >= argv.length || argv[i + 1].startsWith('--')) {
console.error(`[ERROR] Missing value for flag: ${argv[i]}`);
process.exit(1);
}
args[key] = argv[i + 1];
}
return args;
}

function renderTemplate(template, vars) {
let result = template;

// Process conditional sections: {{#key}}...{{/key}}
result = result.replace(/\{\{#(\w+)\}\}\n([\s\S]*?)\{\{\/\1\}\}\n?/g, (_, key, block) => {
const section = vars[key];
if (!section || !section.items || section.items.length === 0) {
return '';
}
const itemList = section.items
.map(item => `- ${String(item).replace(/\{\{/g, '{ {').replace(/\}\}/g, '} }')}`)
.join('\n');
return block.replace(/\{\{items\}\}/g, itemList);
});

// Replace simple variables
result = result.replace(/\{\{(\w+)\}\}/g, (_, key) => {
return vars[key] !== undefined ? vars[key] : '';
});

return result;
}

function main() {
const args = parseArgs(process.argv);

if (!args.target) {
console.error('[ERROR] --target is required');
process.exit(1);
}
if (!args.template) {
console.error('[ERROR] --template is required');
process.exit(1);
}

const targetDir = path.resolve(args.target);
const templatePath = path.resolve(args.template);

// Read template
let template;
try {
template = fs.readFileSync(templatePath, 'utf8');
} catch (err) {
console.error(`[ERROR] Cannot read template: ${err.message}`);
process.exit(1);
}

// Read package.json
let pkg;
try {
pkg = JSON.parse(fs.readFileSync(path.join(targetDir, 'package.json'), 'utf8'));
} catch (err) {
console.error(`[ERROR] Cannot read package.json: ${err.message}`);
process.exit(1);
}

// Extract plugin name (strip @agentsys/ prefix)
const pluginName = (pkg.name || '').replace(/^@agentsys\//, '');
const description = pkg.description || '';

// Read components.json (optional)
let components = { agents: [], skills: [], commands: [] };
const componentsPath = path.join(targetDir, 'components.json');
try {
components = JSON.parse(fs.readFileSync(componentsPath, 'utf8'));
} catch (err) {
if (err.code !== 'ENOENT') {
console.error(`[WARN] Cannot parse components.json: ${err.message}`);
}
}

const vars = {
pluginName,
description,
agents: { items: components.agents || [] },
skills: { items: components.skills || [] },
commands: { items: components.commands || [] },
};

const output = renderTemplate(template, vars);

// Write CLAUDE.md
const outputPath = path.join(targetDir, 'CLAUDE.md');
try {
fs.writeFileSync(outputPath, output, 'utf8');
} catch (err) {
console.error(`[ERROR] Cannot write CLAUDE.md: ${err.message}`);
process.exit(1);
}
console.log(`[OK] Generated ${outputPath}`);
}

main();
Loading