feat: manifest-driven instruction sync to Design Space
- sync-manifest.json: controls which files sync, to which channel - sync-from-manifest.js: reads manifest, dedup by hash, replaces old versions - GitHub Action: auto-syncs on push to main (agents, skills, workflows) - Channels: stable (all), beta (opt-in), internal (not distributed) Push to main → Action runs → Design Space updated → all agents get new version. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1126a0a345
commit
78ec65f0ec
|
|
@ -0,0 +1,28 @@
|
||||||
|
name: Sync Agent Instructions to Design Space
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'src/agents/**'
|
||||||
|
- 'src/skills/**'
|
||||||
|
- 'src/workflows/*/workflow.md'
|
||||||
|
- 'src/sync-manifest.json'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sync:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
|
||||||
|
- name: Sync instructions to Design Space
|
||||||
|
env:
|
||||||
|
DESIGN_SPACE_URL: ${{ secrets.DESIGN_SPACE_URL }}
|
||||||
|
DESIGN_SPACE_ANON_KEY: ${{ secrets.DESIGN_SPACE_ANON_KEY }}
|
||||||
|
WDS_ROOT: ${{ github.workspace }}
|
||||||
|
run: |
|
||||||
|
node tools/sync-from-manifest.js
|
||||||
|
|
@ -0,0 +1,210 @@
|
||||||
|
{
|
||||||
|
"$schema": "./sync-manifest.schema.json",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Controls which agent instructions are synced to Design Space",
|
||||||
|
"updated": "2026-03-24",
|
||||||
|
|
||||||
|
"channels": {
|
||||||
|
"stable": "Production-ready instructions. Synced to all organizations.",
|
||||||
|
"beta": "Experimental features. Opt-in only.",
|
||||||
|
"internal": "Whiteport internal. Not distributed."
|
||||||
|
},
|
||||||
|
|
||||||
|
"instructions": [
|
||||||
|
{
|
||||||
|
"agent": "saga",
|
||||||
|
"type": "persona",
|
||||||
|
"file": "src/agents/saga-analyst.agent.yaml",
|
||||||
|
"channel": "stable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"agent": "freya",
|
||||||
|
"type": "persona",
|
||||||
|
"file": "src/agents/freya-ux.agent.yaml",
|
||||||
|
"channel": "stable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"agent": "saga",
|
||||||
|
"type": "activation",
|
||||||
|
"file": "src/skills/saga.activation.md",
|
||||||
|
"channel": "stable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"agent": "freya",
|
||||||
|
"type": "activation",
|
||||||
|
"file": "src/skills/freya.activation.md",
|
||||||
|
"channel": "stable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"agent": "saga",
|
||||||
|
"type": "skill",
|
||||||
|
"file": "src/skills/saga/SKILL.md",
|
||||||
|
"channel": "stable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"agent": "freya",
|
||||||
|
"type": "skill",
|
||||||
|
"file": "src/skills/freya/SKILL.md",
|
||||||
|
"channel": "stable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"agent": "*",
|
||||||
|
"type": "skill",
|
||||||
|
"file": "src/skills/design-space/SKILL.md",
|
||||||
|
"channel": "stable"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"agent": "saga",
|
||||||
|
"type": "reference",
|
||||||
|
"file": "src/skills/saga/references/discovery-conversation.md",
|
||||||
|
"channel": "stable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"agent": "saga",
|
||||||
|
"type": "reference",
|
||||||
|
"file": "src/skills/saga/references/trigger-mapping.md",
|
||||||
|
"channel": "stable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"agent": "saga",
|
||||||
|
"type": "reference",
|
||||||
|
"file": "src/skills/saga/references/dream-up-approach.md",
|
||||||
|
"channel": "stable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"agent": "saga",
|
||||||
|
"type": "reference",
|
||||||
|
"file": "src/skills/saga/references/strategic-documentation.md",
|
||||||
|
"channel": "stable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"agent": "saga",
|
||||||
|
"type": "reference",
|
||||||
|
"file": "src/skills/saga/references/conversational-followups.md",
|
||||||
|
"channel": "stable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"agent": "saga",
|
||||||
|
"type": "reference",
|
||||||
|
"file": "src/skills/saga/references/seo-strategy-guide.md",
|
||||||
|
"channel": "stable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"agent": "saga",
|
||||||
|
"type": "reference",
|
||||||
|
"file": "src/skills/saga/references/content-structure-principles.md",
|
||||||
|
"channel": "stable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"agent": "saga",
|
||||||
|
"type": "reference",
|
||||||
|
"file": "src/skills/saga/references/inspiration-analysis.md",
|
||||||
|
"channel": "stable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"agent": "saga",
|
||||||
|
"type": "reference",
|
||||||
|
"file": "src/skills/saga/references/working-with-existing-materials.md",
|
||||||
|
"channel": "stable"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"agent": "freya",
|
||||||
|
"type": "reference",
|
||||||
|
"file": "src/skills/freya/references/strategic-design.md",
|
||||||
|
"channel": "stable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"agent": "freya",
|
||||||
|
"type": "reference",
|
||||||
|
"file": "src/skills/freya/references/specification-quality.md",
|
||||||
|
"channel": "stable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"agent": "freya",
|
||||||
|
"type": "reference",
|
||||||
|
"file": "src/skills/freya/references/agentic-development.md",
|
||||||
|
"channel": "stable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"agent": "freya",
|
||||||
|
"type": "reference",
|
||||||
|
"file": "src/skills/freya/references/content-creation.md",
|
||||||
|
"channel": "stable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"agent": "freya",
|
||||||
|
"type": "reference",
|
||||||
|
"file": "src/skills/freya/references/design-system.md",
|
||||||
|
"channel": "stable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"agent": "freya",
|
||||||
|
"type": "reference",
|
||||||
|
"file": "src/skills/freya/references/meta-content-guide.md",
|
||||||
|
"channel": "stable"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"agent": "saga",
|
||||||
|
"type": "workflow",
|
||||||
|
"file": "src/workflows/0-alignment-signoff/workflow.md",
|
||||||
|
"channel": "stable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"agent": "*",
|
||||||
|
"type": "workflow",
|
||||||
|
"file": "src/workflows/0-project-setup/workflow.md",
|
||||||
|
"channel": "stable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"agent": "saga",
|
||||||
|
"type": "workflow",
|
||||||
|
"file": "src/workflows/1-project-brief/workflow.md",
|
||||||
|
"channel": "stable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"agent": "saga",
|
||||||
|
"type": "workflow",
|
||||||
|
"file": "src/workflows/2-trigger-mapping/workflow.md",
|
||||||
|
"channel": "stable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"agent": "freya",
|
||||||
|
"type": "workflow",
|
||||||
|
"file": "src/workflows/3-scenarios/workflow.md",
|
||||||
|
"channel": "stable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"agent": "freya",
|
||||||
|
"type": "workflow",
|
||||||
|
"file": "src/workflows/4-ux-design/workflow.md",
|
||||||
|
"channel": "stable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"agent": "mimir",
|
||||||
|
"type": "workflow",
|
||||||
|
"file": "src/workflows/5-agentic-development/workflow.md",
|
||||||
|
"channel": "stable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"agent": "freya",
|
||||||
|
"type": "workflow",
|
||||||
|
"file": "src/workflows/6-asset-generation/workflow.md",
|
||||||
|
"channel": "stable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"agent": "freya",
|
||||||
|
"type": "workflow",
|
||||||
|
"file": "src/workflows/7-design-system/workflow.md",
|
||||||
|
"channel": "stable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"agent": "idunn",
|
||||||
|
"type": "workflow",
|
||||||
|
"file": "src/workflows/8-product-evolution/workflow.md",
|
||||||
|
"channel": "stable"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,171 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Sync agent instructions from WDS repo to Design Space.
|
||||||
|
* Reads sync-manifest.json to determine what to sync.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* node sync-from-manifest.js # sync all stable
|
||||||
|
* node sync-from-manifest.js --channel beta # sync beta channel
|
||||||
|
* node sync-from-manifest.js --agent saga # sync one agent
|
||||||
|
* node sync-from-manifest.js --dry-run # preview only
|
||||||
|
*
|
||||||
|
* Environment:
|
||||||
|
* DESIGN_SPACE_URL Supabase project URL
|
||||||
|
* DESIGN_SPACE_ANON_KEY Supabase anon key
|
||||||
|
* WDS_ROOT Path to WDS repo (default: auto-detect)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { readFileSync, existsSync } from 'fs';
|
||||||
|
import { join, basename, dirname } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { createHash } from 'crypto';
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const REPO_ROOT = process.env.WDS_ROOT || join(__dirname, '..');
|
||||||
|
|
||||||
|
// --- Load env ---
|
||||||
|
for (const envPath of [join(REPO_ROOT, '.env'), join(__dirname, '../../design-space/.env')]) {
|
||||||
|
if (existsSync(envPath)) {
|
||||||
|
for (const line of readFileSync(envPath, 'utf8').replace(/\r/g, '').split('\n')) {
|
||||||
|
const match = line.match(/^([^#=]+)=(.*)$/);
|
||||||
|
if (match && !process.env[match[1].trim()]) {
|
||||||
|
process.env[match[1].trim()] = match[2].trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const SUPABASE_URL = process.env.DESIGN_SPACE_URL;
|
||||||
|
const SUPABASE_KEY = process.env.DESIGN_SPACE_ANON_KEY;
|
||||||
|
|
||||||
|
if (!SUPABASE_URL || !SUPABASE_KEY) {
|
||||||
|
console.error('Missing DESIGN_SPACE_URL or DESIGN_SPACE_ANON_KEY');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Args ---
|
||||||
|
const DRY_RUN = process.argv.includes('--dry-run');
|
||||||
|
const CHANNEL = (() => {
|
||||||
|
const idx = process.argv.indexOf('--channel');
|
||||||
|
return idx !== -1 ? process.argv[idx + 1] : 'stable';
|
||||||
|
})();
|
||||||
|
const AGENT_FILTER = (() => {
|
||||||
|
const idx = process.argv.indexOf('--agent');
|
||||||
|
return idx !== -1 ? process.argv[idx + 1] : null;
|
||||||
|
})();
|
||||||
|
|
||||||
|
// --- Load manifest ---
|
||||||
|
const manifestPath = join(REPO_ROOT, 'src/sync-manifest.json');
|
||||||
|
if (!existsSync(manifestPath)) {
|
||||||
|
console.error(`Manifest not found: ${manifestPath}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
||||||
|
console.log(`WDS Instruction Sync v${manifest.version}`);
|
||||||
|
console.log(`Repo: ${REPO_ROOT}`);
|
||||||
|
console.log(`Channel: ${CHANNEL}`);
|
||||||
|
if (AGENT_FILTER) console.log(`Agent filter: ${AGENT_FILTER}`);
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
// --- Filter and collect ---
|
||||||
|
const filtered = manifest.instructions.filter(i => {
|
||||||
|
if (i.channel !== CHANNEL) return false;
|
||||||
|
if (AGENT_FILTER && i.agent !== AGENT_FILTER && i.agent !== '*') return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`${filtered.length} instructions to sync`);
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
// --- Process ---
|
||||||
|
let uploaded = 0;
|
||||||
|
let skipped = 0;
|
||||||
|
let failed = 0;
|
||||||
|
|
||||||
|
for (const instr of filtered) {
|
||||||
|
const filePath = join(REPO_ROOT, instr.file);
|
||||||
|
if (!existsSync(filePath)) {
|
||||||
|
console.warn(` SKIP (not found): ${instr.file}`);
|
||||||
|
skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = readFileSync(filePath, 'utf8');
|
||||||
|
const hash = createHash('sha256').update(content).digest('hex').substring(0, 12);
|
||||||
|
const name = basename(instr.file);
|
||||||
|
|
||||||
|
if (DRY_RUN) {
|
||||||
|
console.log(` [${instr.agent}] ${instr.type}/${name} (${content.length} chars, hash: ${hash})`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if this exact version exists (by source_file + hash in metadata)
|
||||||
|
const checkRes = await fetch(`${SUPABASE_URL}/rest/v1/design_space?select=id,metadata&category=eq.agent_instruction&source_file=eq.${instr.file.replace(/\//g, '%2F')}&limit=1`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${SUPABASE_KEY}`,
|
||||||
|
'apikey': SUPABASE_KEY,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const existing = await checkRes.json();
|
||||||
|
|
||||||
|
if (existing?.[0]?.metadata?.hash === hash) {
|
||||||
|
skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete old version if exists
|
||||||
|
if (existing?.[0]?.id) {
|
||||||
|
await fetch(`${SUPABASE_URL}/rest/v1/design_space?id=eq.${existing[0].id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${SUPABASE_KEY}`,
|
||||||
|
'apikey': SUPABASE_KEY,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload new version
|
||||||
|
const res = await fetch(`${SUPABASE_URL}/functions/v1/capture-design-space`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${SUPABASE_KEY}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
content,
|
||||||
|
category: 'agent_instruction',
|
||||||
|
project: 'wds',
|
||||||
|
designer: instr.agent,
|
||||||
|
topics: [instr.type, instr.agent, `channel:${instr.channel}`],
|
||||||
|
components: [name],
|
||||||
|
source: 'sync-manifest',
|
||||||
|
source_file: instr.file,
|
||||||
|
metadata: {
|
||||||
|
agent: instr.agent,
|
||||||
|
type: instr.type,
|
||||||
|
name,
|
||||||
|
layer: 'framework',
|
||||||
|
channel: instr.channel,
|
||||||
|
hash,
|
||||||
|
manifest_version: manifest.version,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
uploaded++;
|
||||||
|
console.log(` OK [${instr.agent}] ${instr.type}/${name}`);
|
||||||
|
} else {
|
||||||
|
failed++;
|
||||||
|
console.error(` FAIL [${instr.agent}] ${name}: ${await res.text()}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
failed++;
|
||||||
|
console.error(` FAIL [${instr.agent}] ${name}: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log();
|
||||||
|
console.log(`Done: ${uploaded} uploaded, ${skipped} unchanged, ${failed} failed`);
|
||||||
|
if (failed > 0) process.exit(1);
|
||||||
Loading…
Reference in New Issue