v0.8.0 #20
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build Claude.ai Zips | |
| on: | |
| workflow_dispatch: | |
| release: | |
| types: [created] | |
| permissions: | |
| contents: write | |
| jobs: | |
| build: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Build zips | |
| run: | | |
| mkdir -p dist | |
| # Stage 1: Per-region zips (existing structure) | |
| for region in africa americas asia europe oceania; do | |
| zip -r dist/cultures-${region}.zip regions/${region}/ | |
| done | |
| # Stage 2: Full cultures zip -- built as flat zip in Stage 5 (Python block below) | |
| # Stage 3: Per-engine platform zips (shared stack + platform-specific instructions) | |
| for platform in claude copilot gemini; do | |
| zip -j dist/engine_${platform}.zip \ | |
| engine/${platform}/instructions.md \ | |
| engine/stack.md \ | |
| engine/process_world_is_spinning.md | |
| done | |
| # Stage 4: Culture-specific zips for Claude consumption (new) | |
| # Auto-detect complete cultures (has position.md + personas + place + piece + README) | |
| # and create flat zips with all culture files + engine files | |
| python3 << 'PYTHON_EOF' | |
| import os | |
| import re | |
| import zipfile | |
| import shutil | |
| from pathlib import Path | |
| def is_complete_culture(culture_dir): | |
| """Check if culture folder has all required components.""" | |
| try: | |
| culture_path = Path(culture_dir) | |
| # Must have position file | |
| has_position = any(culture_path.glob('culture_*_position.md')) | |
| # Must have at least 2 personas | |
| personas = list(culture_path.glob('culture_*_persona_*.md')) + list(culture_path.glob('persona_*.md')) | |
| has_personas = len(personas) >= 2 | |
| # Must have at least 1 place | |
| places = list(culture_path.glob('culture_*_place_*.md')) | |
| has_places = len(places) >= 1 | |
| # Must have at least 1 piece | |
| pieces = list(culture_path.glob('culture_*_piece_*.md')) | |
| has_pieces = len(pieces) >= 1 | |
| # Must have README | |
| has_readme = (culture_path / 'README.md').exists() | |
| return has_position and has_personas and has_places and has_pieces and has_readme | |
| except: | |
| return False | |
| def is_region_manifest(region_dir): | |
| """Check if region folder is a manifest (has README.md at root).""" | |
| return (region_dir / 'README.md').exists() | |
| def rewrite_links_for_flat_zip(content): | |
| """Rewrite links from nested folder structure to flat structure. | |
| Culture files are stored in regions/europe/germany/ etc. (nested depth varies). | |
| Their links reference other files using relative paths like: | |
| - ../../engine/stack.md (traverses up to repo root, then into engine/) | |
| - ../../../engine/instructions.md (varies by depth) | |
| In the flat zip structure (culture/ subfolder), all culture files are together. | |
| Links should be simplified to just filenames: | |
| - ../../engine/stack.md → stack.md | |
| - Removes all ../ traversal and keeps just the filename | |
| """ | |
| import re | |
| # Strip all ../ or ..\\ traversal and keep just the filename | |
| # Pattern: [text](../ or .../ traversal/path/filename.md) → [text](filename.md) | |
| content = re.sub( | |
| r'\]\((?:\.\./)+([^)]+\.md)\)', | |
| lambda m: f']({m.group(1).split("/")[-1]})', | |
| content | |
| ) | |
| return content | |
| def strip_markdown_footer(content): | |
| """Strip trailing metadata footer blocks from markdown for deployment artifacts. | |
| Expected footer styles include: | |
| - v2 footer blocks with italic metadata lines | |
| - legacy Hofstede signal lines | |
| - plain version lines like: v0.2.0 - KAI Worlds | |
| """ | |
| lines = content.splitlines() | |
| if not lines: | |
| return content | |
| end = len(lines) - 1 | |
| while end >= 0 and not lines[end].strip(): | |
| end -= 1 | |
| if end < 0: | |
| return content | |
| def unwrap_italic(s): | |
| if len(s) >= 2 and s.startswith('*') and s.endswith('*'): | |
| return s[1:-1].strip() | |
| return s | |
| def is_footer_metadata_line(s): | |
| stripped = s.strip() | |
| if not stripped: | |
| return False | |
| core = unwrap_italic(stripped) | |
| lower = core.lower() | |
| if lower.startswith('khai:'): | |
| return True | |
| if lower.startswith('hofstede:'): | |
| return True | |
| if lower.startswith('hofstede signal:'): | |
| return True | |
| # Legacy per-file stamp such as: culture_spanish_place_madrid.md - 13.05.2026 | |
| if re.match(r'^culture_[a-z0-9_\-]+\.md\s*-\s*\d{1,2}[./-]\d{1,2}[./-]\d{2,4}$', lower): | |
| return True | |
| # v2 IP safeguard line: YYYY-MM-DD | owner | version | license | |
| if re.match(r'^\d{4}-\d{2}-\d{2}\s*\|\s*.+\|\s*v\d+\.\d+\.\d+\s*\|\s*.+$', core, re.IGNORECASE): | |
| return True | |
| # Plain or italic version lines. | |
| if re.match(r'^v\d+\.\d+\.\d+\s*-\s*(kai worlds|kai labs|kai hacks ai|cultures).*$' , core, re.IGNORECASE): | |
| return True | |
| return False | |
| i = end | |
| stripped_any = False | |
| while i >= 0: | |
| stripped = lines[i].strip() | |
| if not stripped: | |
| i -= 1 | |
| continue | |
| if is_footer_metadata_line(lines[i]): | |
| stripped_any = True | |
| i -= 1 | |
| continue | |
| break | |
| if stripped_any: | |
| kept = lines[:i + 1] | |
| while kept and not kept[-1].strip(): | |
| kept.pop() | |
| if kept and kept[-1].strip() == '---': | |
| kept.pop() | |
| while kept and not kept[-1].strip(): | |
| kept.pop() | |
| return '\n'.join(kept) + '\n' | |
| return '\n'.join(lines[:end + 1]) + ('\n' if content.endswith('\n') else '') | |
| def strip_footers_in_folder(folder: Path): | |
| """Strip deployment footers from all markdown files in a folder.""" | |
| for markdown_file in folder.glob('*.md'): | |
| try: | |
| content = markdown_file.read_text(encoding='utf-8') | |
| stripped = strip_markdown_footer(content) | |
| markdown_file.write_text(stripped, encoding='utf-8') | |
| except: | |
| pass | |
| def create_engine_platform_zip(platform: str): | |
| """Create per-platform engine zip with Claude footer stripping.""" | |
| zip_path = Path(f'dist/engine_{platform}.zip') | |
| files = [ | |
| (Path('engine') / platform / 'instructions.md', 'instructions.md'), | |
| (Path('engine') / 'stack.md', 'stack.md'), | |
| (Path('engine') / 'process_world_is_spinning.md', 'process_world_is_spinning.md'), | |
| ] | |
| with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf: | |
| for src_path, arcname in files: | |
| if not src_path.exists(): | |
| continue | |
| content = src_path.read_text(encoding='utf-8') | |
| if platform == 'claude': | |
| content = strip_markdown_footer(content) | |
| zf.writestr(arcname, content) | |
| print(f'Created engine_{platform}.zip (footer strip: {platform == "claude"})') | |
| def create_culture_zip(staging_dir, zip_name): | |
| """Helper to create a single culture zip from staging directory.""" | |
| zip_path = Path(f'dist/{zip_name}.zip') | |
| with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf: | |
| # Add known root files | |
| for root_file in ['LICENSE', 'README.md', 'REFERENCES.md']: | |
| file_path = staging_dir / root_file | |
| if file_path.exists(): | |
| zf.write(file_path, arcname=root_file) | |
| # Add any hofstede_* files at root (country zips) | |
| for hofstede_file in sorted(staging_dir.glob('hofstede_*')): | |
| zf.write(hofstede_file, arcname=hofstede_file.name) | |
| # Add hofstede/ subfolder files (regional zips) | |
| hofstede_dir = staging_dir / 'hofstede' | |
| if hofstede_dir.exists(): | |
| for hofstede_file in sorted(hofstede_dir.glob('*')): | |
| zf.write(hofstede_file, arcname=f'hofstede/{hofstede_file.name}') | |
| # Add engine/ subdirectory files | |
| for engine_file in (staging_dir / 'engine').glob('*.md'): | |
| zf.write(engine_file, arcname=f'engine/{engine_file.name}') | |
| # Add culture/ subdirectory files | |
| culture_files = list((staging_dir / 'culture').glob('*.md')) | |
| for culture_file in culture_files: | |
| zf.write(culture_file, arcname=f'culture/{culture_file.name}') | |
| return len(culture_files) | |
| def stage_culture_files(staging_dir, culture_dir, culture_name): | |
| """Stage culture files into the staging directory.""" | |
| engine_path = Path('engine') | |
| # Copy instructions.md from claude-specific or fallback to generic | |
| claude_instructions = engine_path / 'claude' / 'instructions.md' | |
| if claude_instructions.exists(): | |
| shutil.copy2(claude_instructions, staging_dir / 'engine' / 'instructions.md') | |
| # Copy stack.md | |
| stack_src = engine_path / 'stack.md' | |
| if stack_src.exists(): | |
| shutil.copy2(stack_src, staging_dir / 'engine' / 'stack.md') | |
| # Copy culture/ files (all culture_*.md files from country dir) | |
| culture_count = 0 | |
| for culture_file in culture_dir.glob('culture_*.md'): | |
| shutil.copy2(culture_file, staging_dir / 'culture' / culture_file.name) | |
| culture_count += 1 | |
| # Rewrite links in CULTURE FILES ONLY (strip ../ traversal for flat structure) | |
| for markdown_file in (staging_dir / 'culture').glob('*.md'): | |
| try: | |
| content = markdown_file.read_text(encoding='utf-8') | |
| content = rewrite_links_for_flat_zip(content) | |
| markdown_file.write_text(content, encoding='utf-8') | |
| except: | |
| pass | |
| # Strip footers for Claude deployment in staged culture and engine files | |
| strip_footers_in_folder(staging_dir / 'culture') | |
| strip_footers_in_folder(staging_dir / 'engine') | |
| return culture_count | |
| # Rebuild Stage 3 engine zips in Python so Claude engine files can be footer-stripped. | |
| for platform in ['claude', 'copilot', 'gemini']: | |
| create_engine_platform_zip(platform) | |
| # Stage 4a: Individual country culture zips | |
| regions_path = Path('regions') | |
| for region_dir in regions_path.iterdir(): | |
| if not region_dir.is_dir(): | |
| continue | |
| for country_dir in region_dir.iterdir(): | |
| if not country_dir.is_dir(): | |
| continue | |
| if is_complete_culture(country_dir): | |
| # Determine culture name from folder | |
| culture_name = country_dir.name | |
| zip_name = f'cultures-{region_dir.name}-{culture_name}' | |
| # Create temp staging directory with structure | |
| staging_dir = Path(f'dist/staging-{culture_name}') | |
| if staging_dir.exists(): | |
| shutil.rmtree(staging_dir) | |
| staging_dir.mkdir(parents=True, exist_ok=True) | |
| # Create subdirectories: engine/, culture/ | |
| (staging_dir / 'engine').mkdir(exist_ok=True) | |
| (staging_dir / 'culture').mkdir(exist_ok=True) | |
| # Copy root scaffold files (LICENSE, README, REFERENCES) | |
| if Path('LICENSE').exists(): | |
| shutil.copy2('LICENSE', staging_dir / 'LICENSE') | |
| # Copy README from country dir (baseline for each culture) | |
| readme_src = country_dir / 'README.md' | |
| if readme_src.exists(): | |
| readme_content = readme_src.read_text(encoding='utf-8') | |
| # Strip Download section: remove everything up to and including first --- divider | |
| # This keeps Install + Content sections | |
| if '---' in readme_content: | |
| parts = readme_content.split('---', 2) # Split on first two occurrences | |
| if len(parts) >= 3: | |
| readme_content = '---'.join(parts[1:]).lstrip('\n') # Keep from second --- onward | |
| # Rewrite links (flatten nested paths) | |
| readme_content = rewrite_links_for_flat_zip(readme_content) | |
| (staging_dir / 'README.md').write_text(readme_content, encoding='utf-8') | |
| # Copy REFERENCES from country dir (baseline for each culture) | |
| references_src = country_dir / 'REFERENCES.md' | |
| if references_src.exists(): | |
| shutil.copy2(references_src, staging_dir / 'REFERENCES.md') | |
| # Copy hofstede files from country dir to zip root | |
| for hofstede_file in country_dir.glob('hofstede_*'): | |
| shutil.copy2(hofstede_file, staging_dir / hofstede_file.name) | |
| # Stage engine + culture files | |
| culture_count = stage_culture_files(staging_dir, country_dir, culture_name) | |
| # Create structured zip | |
| create_culture_zip(staging_dir, zip_name) | |
| # Cleanup staging | |
| shutil.rmtree(staging_dir) | |
| print(f'Created {zip_name}.zip with structured layout (LICENSE + README + REFERENCES + engine/ + culture/{culture_count} files)') | |
| # Stage 4b: Regional combined zips (manifests that combine multiple countries) | |
| for region_dir in regions_path.iterdir(): | |
| if not region_dir.is_dir(): | |
| continue | |
| if is_region_manifest(region_dir): | |
| region_name = region_dir.name | |
| # Find all complete cultures in this region | |
| complete_cultures = [] | |
| for country_dir in region_dir.iterdir(): | |
| if country_dir.is_dir() and is_complete_culture(country_dir): | |
| complete_cultures.append(country_dir) | |
| # Only create regional zip if there are complete cultures | |
| if complete_cultures: | |
| # Create staging directory for combined regional zip | |
| staging_dir = Path(f'dist/staging-{region_name}') | |
| if staging_dir.exists(): | |
| shutil.rmtree(staging_dir) | |
| staging_dir.mkdir(parents=True, exist_ok=True) | |
| # Create subdirectories | |
| (staging_dir / 'engine').mkdir(exist_ok=True) | |
| (staging_dir / 'culture').mkdir(exist_ok=True) | |
| (staging_dir / 'hofstede').mkdir(exist_ok=True) | |
| # Copy root files | |
| if Path('LICENSE').exists(): | |
| shutil.copy2('LICENSE', staging_dir / 'LICENSE') | |
| # Copy region README (manifest) | |
| region_readme = region_dir / 'README.md' | |
| if region_readme.exists(): | |
| readme_content = region_readme.read_text(encoding='utf-8') | |
| readme_content = rewrite_links_for_flat_zip(readme_content) | |
| (staging_dir / 'README.md').write_text(readme_content, encoding='utf-8') | |
| # Copy engine files | |
| engine_path = Path('engine') | |
| claude_instructions = engine_path / 'claude' / 'instructions.md' | |
| if claude_instructions.exists(): | |
| shutil.copy2(claude_instructions, staging_dir / 'engine' / 'instructions.md') | |
| stack_src = engine_path / 'stack.md' | |
| if stack_src.exists(): | |
| shutil.copy2(stack_src, staging_dir / 'engine' / 'stack.md') | |
| # Combine all culture files from all complete countries | |
| total_culture_count = 0 | |
| for country_dir in complete_cultures: | |
| for culture_file in country_dir.glob('culture_*.md'): | |
| dest_name = culture_file.name | |
| shutil.copy2(culture_file, staging_dir / 'culture' / dest_name) | |
| total_culture_count += 1 | |
| # Copy hofstede files with country prefix: hofstede_{country}_{suffix} | |
| for hofstede_file in sorted(country_dir.glob('hofstede_*')): | |
| suffix = hofstede_file.name[len('hofstede_'):] | |
| dest_name = f'hofstede_{country_dir.name}_{suffix}' | |
| shutil.copy2(hofstede_file, staging_dir / 'hofstede' / dest_name) | |
| # Rewrite links in all culture files | |
| for markdown_file in (staging_dir / 'culture').glob('*.md'): | |
| try: | |
| content = markdown_file.read_text(encoding='utf-8') | |
| content = rewrite_links_for_flat_zip(content) | |
| markdown_file.write_text(content, encoding='utf-8') | |
| except: | |
| pass | |
| # Strip footers for Claude deployment in staged culture and engine files | |
| strip_footers_in_folder(staging_dir / 'culture') | |
| strip_footers_in_folder(staging_dir / 'engine') | |
| # Create regional combined zip | |
| culture_count = create_culture_zip(staging_dir, region_name) | |
| # Cleanup staging | |
| shutil.rmtree(staging_dir) | |
| print(f'Created {region_name}.zip as regional combined manifest ({len(complete_cultures)} countries, {total_culture_count} culture files)') | |
| # Stage 5: World flat zip (all complete cultures across all regions) | |
| staging_dir = Path('dist/staging-world') | |
| if staging_dir.exists(): | |
| shutil.rmtree(staging_dir) | |
| staging_dir.mkdir(parents=True, exist_ok=True) | |
| (staging_dir / 'engine').mkdir(exist_ok=True) | |
| (staging_dir / 'culture').mkdir(exist_ok=True) | |
| (staging_dir / 'hofstede').mkdir(exist_ok=True) | |
| # Root files | |
| if Path('LICENSE').exists(): | |
| shutil.copy2('LICENSE', staging_dir / 'LICENSE') | |
| if Path('README.md').exists(): | |
| readme_content = Path('README.md').read_text(encoding='utf-8') | |
| readme_content = rewrite_links_for_flat_zip(readme_content) | |
| (staging_dir / 'README.md').write_text(readme_content, encoding='utf-8') | |
| # Engine files | |
| claude_instructions = Path('engine') / 'claude' / 'instructions.md' | |
| if claude_instructions.exists(): | |
| shutil.copy2(claude_instructions, staging_dir / 'engine' / 'instructions.md') | |
| stack_src = Path('engine') / 'stack.md' | |
| if stack_src.exists(): | |
| shutil.copy2(stack_src, staging_dir / 'engine' / 'stack.md') | |
| # All complete cultures across all regions | |
| world_culture_count = 0 | |
| world_hofstede_count = 0 | |
| for region_dir in regions_path.iterdir(): | |
| if not region_dir.is_dir(): | |
| continue | |
| for country_dir in region_dir.iterdir(): | |
| if not country_dir.is_dir() or not is_complete_culture(country_dir): | |
| continue | |
| for culture_file in country_dir.glob('culture_*.md'): | |
| shutil.copy2(culture_file, staging_dir / 'culture' / culture_file.name) | |
| world_culture_count += 1 | |
| for hofstede_file in sorted(country_dir.glob('hofstede_*')): | |
| suffix = hofstede_file.name[len('hofstede_'):] | |
| dest_name = f'hofstede_{country_dir.name}_{suffix}' | |
| shutil.copy2(hofstede_file, staging_dir / 'hofstede' / dest_name) | |
| world_hofstede_count += 1 | |
| # Rewrite links in all culture files | |
| for markdown_file in (staging_dir / 'culture').glob('*.md'): | |
| try: | |
| content = markdown_file.read_text(encoding='utf-8') | |
| content = rewrite_links_for_flat_zip(content) | |
| markdown_file.write_text(content, encoding='utf-8') | |
| except: | |
| pass | |
| # Strip footers for Claude deployment in staged culture and engine files | |
| strip_footers_in_folder(staging_dir / 'culture') | |
| strip_footers_in_folder(staging_dir / 'engine') | |
| create_culture_zip(staging_dir, 'cultures-full') | |
| shutil.rmtree(staging_dir) | |
| print(f'Created cultures-full.zip as world flat zip ({world_culture_count} culture files, {world_hofstede_count} hofstede files)') | |
| PYTHON_EOF | |
| - name: Upload to release | |
| if: github.event_name == 'release' | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| files: dist/*.zip | |
| - name: Upload as artifacts | |
| if: github.event_name == 'workflow_dispatch' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: cultures-zips | |
| path: dist/*.zip | |
| retention-days: 7 |