From 78ec65f0ecdfcd436dc170ad8c3d304a58129663 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Angner?= Date: Tue, 24 Mar 2026 16:10:12 +0100 Subject: [PATCH] feat: manifest-driven instruction sync to Design Space MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- .github/workflows/sync-instructions.yml | 28 ++++ src/sync-manifest.json | 210 ++++++++++++++++++++++++ tools/sync-from-manifest.js | 171 +++++++++++++++++++ 3 files changed, 409 insertions(+) create mode 100644 .github/workflows/sync-instructions.yml create mode 100644 src/sync-manifest.json create mode 100644 tools/sync-from-manifest.js diff --git a/.github/workflows/sync-instructions.yml b/.github/workflows/sync-instructions.yml new file mode 100644 index 000000000..c83391613 --- /dev/null +++ b/.github/workflows/sync-instructions.yml @@ -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 diff --git a/src/sync-manifest.json b/src/sync-manifest.json new file mode 100644 index 000000000..ff45bc956 --- /dev/null +++ b/src/sync-manifest.json @@ -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" + } + ] +} diff --git a/tools/sync-from-manifest.js b/tools/sync-from-manifest.js new file mode 100644 index 000000000..d2d703cc6 --- /dev/null +++ b/tools/sync-from-manifest.js @@ -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);