#!/usr/bin/env bash # scripts/update-agent-docs.sh # # Maintains AI agent docs across the ByteLyst workspace with ZERO duplication. # # Architecture (locked 2026-05-23): # Canonical source of behavior rules lives in ONE file: # learning_ai_common_plat/AI.dev/SKILLS/agent-behavior-guidelines.md # # Per-repo files (managed by this script): # AGENTS.md — repo-specific (hand-maintained). # Script idempotently prepends a "Read first" # pointer block pointing to the canonical file. # .github/copilot-instructions.md — thin pointer (5 lines). Regenerated. # .aider.conf.yml — Aider config (reads AGENTS.md). Regenerated. # .editorconfig — editor config. Regenerated. # # Legacy files deleted by this script (they used to duplicate AGENTS.md content): # .cursorrules, .windsurfrules, .clinerules, CLAUDE.md # # Usage: # ./scripts/update-agent-docs.sh # apply changes + commit per repo # ./scripts/update-agent-docs.sh --dry-run # show what would change, no writes # ./scripts/update-agent-docs.sh --no-commit # write files but don't git commit set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPOS_TXT="${SCRIPT_DIR}/../.windsurf/workflows/repos.txt" BASE_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)" CANONICAL_REL_FROM_COMMON_PLAT="AI.dev/SKILLS/agent-behavior-guidelines.md" DRY_RUN=false NO_COMMIT=false CHANGED_REPOS=() for arg in "$@"; do case "$arg" in --dry-run) DRY_RUN=true ;; --no-commit) NO_COMMIT=true ;; -h|--help) sed -n '2,25p' "${BASH_SOURCE[0]}" | sed 's/^# \?//' exit 0 ;; esac done # ── colours ──────────────────────────────────────────────────────────────── RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' BLUE='\033[0;34m'; CYAN='\033[0;36m'; NC='\033[0m' info() { echo -e "${BLUE}[INFO]${NC} $*"; } ok() { echo -e "${GREEN}[OK]${NC} $*"; } warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } err() { echo -e "${RED}[ERR]${NC} $*" >&2; } header(){ echo -e "\n${CYAN}══ $* ══${NC}"; } # ── per-repo metadata (only fields still needed) ─────────────────────────── # NAME — display name for headings # ID — productId string # STACK — short stack description # BUILD_VFY — single command shown in copilot pointer & aider.conf # LINT1-3 — optional lint commands for aider.conf # AIDER_READ2 — second file aider should read alongside AGENTS.md set_meta() { NAME="" ID="" STACK="" BUILD_VFY="" LINT1="" LINT2="" LINT3="" AIDER_READ2="README.md" case "$1" in learning_ai_common_plat) NAME="@bytelyst Common Platform"; ID="(product-agnostic)" STACK="TypeScript, ESM, pnpm workspace, Fastify 5, Vitest, Azure Cosmos DB" BUILD_VFY="pnpm build && pnpm test && pnpm typecheck" LINT1="pnpm build 2>&1 | tail -10" LINT2="pnpm test 2>&1 | tail -10" LINT3="pnpm typecheck 2>&1 | tail -10" ;; learning_voice_ai_agent) NAME="LysnrAI"; ID="lysnrai" STACK="Python 3.12 (desktop/backend) + Next.js 16 (dashboards) + Fastify 5 (microservices in sibling repo)" BUILD_VFY="python -m pytest tests/ -v --tb=short" LINT1="cd user-dashboard-web && npx tsc --noEmit 2>&1 | tail -10" LINT2="python -m pytest tests/ -x --tb=short 2>&1 | tail -10" AIDER_READ2="README_MONO_REPO.md" ;; learning_multimodal_memory_agents) NAME="MindLyst"; ID="mindlyst" STACK="KMP (shared) + SwiftUI (iOS) + Jetpack Compose (Android) + Next.js 16 (web)" BUILD_VFY="./gradlew :shared:compileKotlinIosSimulatorArm64" LINT1="cd mindlyst-native && ./gradlew :shared:compileKotlinIosSimulatorArm64 2>&1 | tail -5" LINT2="cd mindlyst-native/web && npx next build 2>&1 | tail -10" AIDER_READ2="ARCHITECTURE.md" ;; learning_ai_clock) NAME="ChronoMind"; ID="chronomind" STACK="Next.js 16 (web) + SwiftUI (iOS/Watch/Mac) + Jetpack Compose (Android) + Fastify 5 (backend)" BUILD_VFY="cd web && npm test && npm run typecheck && npm run build" LINT1="cd web && npm test 2>&1 | tail -10" LINT2="cd web && npm run typecheck 2>&1 | tail -10" AIDER_READ2="docs/PRD.md" ;; learning_ai_fastgap) NAME="NomGap"; ID="nomgap" STACK="React Native (Expo SDK 55) + TypeScript + Fastify 5 (backend)" BUILD_VFY="npm test && npm run typecheck" LINT1="npm test 2>&1 | tail -10" LINT2="npm run typecheck 2>&1 | tail -10" AIDER_READ2="docs/PRD.md" ;; learning_ai_jarvis_jr) NAME="JarvisJr"; ID="jarvisjr" STACK="SwiftUI (iOS/Watch/Mac) + Next.js 16 (web) + Jetpack Compose (Android) + Fastify 5 (backend)" BUILD_VFY="cd web && npm test && npm run typecheck && npm run build" LINT1="cd web && npm test 2>&1 | tail -10" LINT2="cd web && npm run typecheck 2>&1 | tail -10" AIDER_READ2="docs/ENHANCED_IDEA_v2.md" ;; learning_ai_peakpulse) NAME="PeakPulse"; ID="peakpulse" STACK="SwiftUI (iOS 17+), SwiftData, MapKit, WeatherKit, ActivityKit, WidgetKit, App Intents" BUILD_VFY="cd ios && xcodegen generate && xcodebuild -scheme PeakPulse -sdk iphonesimulator build" LINT1="cd ios && xcodebuild -scheme PeakPulse -sdk iphonesimulator build 2>&1 | tail -20" AIDER_READ2="docs/PRD.md" ;; learning_ai_notes) NAME="ByteLyst Agentic Notes"; ID="bytelyst-notes" STACK="Docs + Fastify 5 backend scaffold with TypeScript ESM and @bytelyst/* shared packages" BUILD_VFY="cd backend && npm test && npm run typecheck && npm run build" LINT1="cd backend && npm test 2>&1 | tail -10" LINT2="cd backend && npm run typecheck 2>&1 | tail -10" LINT3="cd backend && npm run build 2>&1 | tail -10" AIDER_READ2="docs/ROADMAP.md" ;; learning_ai_flowmonk) NAME="FlowMonk"; ID="flowmonk" STACK="Next.js 16 (web) + React Native/Expo (mobile) + Fastify 5 (backend)" BUILD_VFY="cd backend && npm run typecheck && npm run build && cd ../web && npm run typecheck && npm run build && cd ../mobile && npm run typecheck" LINT1="cd backend && npm run typecheck 2>&1 | tail -10" LINT2="cd web && npm run typecheck 2>&1 | tail -10" LINT3="cd mobile && npm run typecheck 2>&1 | tail -10" AIDER_READ2="docs/ROADMAP.md" ;; learning_ai_trails) NAME="ActionTrail"; ID="actiontrail" STACK="Next.js 16 (web) + Fastify 5 (backend) + TypeScript ESM" BUILD_VFY="cd backend && npm test && npm run typecheck && cd ../sdk && npm test && npm run typecheck && cd ../web && npm run typecheck" LINT1="cd backend && npm test 2>&1 | tail -10" LINT2="cd backend && npm run typecheck 2>&1 | tail -10" LINT3="cd sdk && npm run typecheck 2>&1 | tail -10" AIDER_READ2="docs/roadmap.md" ;; learning_ai_local_memory_gpt) NAME="Local Memory GPT"; ID="localmemgpt" STACK="Fastify 5 + TypeScript ESM (backend) + Next.js 16 (web) + SQLite + Ollama" BUILD_VFY="cd backend && npm test && npm run typecheck && npm run build && cd ../web && npm run typecheck && npm run build" LINT1="cd backend && npm test 2>&1 | tail -10" LINT2="cd backend && npm run typecheck 2>&1 | tail -10" LINT3="cd web && npm run typecheck 2>&1 | tail -10" ;; learning_ai_auth_app) NAME="ByteLyst SmartAuth"; ID="smartauth" STACK="SwiftUI (iOS/Watch) + Jetpack Compose (Android) companion app + PRD/roadmap for auth features extending platform-service in common-plat" BUILD_VFY="cd ios && xcodegen generate && xcodebuild -scheme ByteLystAuth -sdk iphonesimulator build && cd ../android && ./gradlew :app:assembleDebug" LINT1="cd ios && xcodegen generate >/dev/null 2>&1 && xcodebuild -scheme ByteLystAuth -sdk iphonesimulator build 2>&1 | tail -20" LINT2="cd android && ./gradlew :app:assembleDebug 2>&1 | tail -20" ;; learning_ai_efforise) NAME="EffoRise"; ID="efforise" STACK="Vite + React 19 SPA (client/) + Fastify 5 backend (backend/) + React Native/Expo (mobile/) + @bytelyst/* shared packages" BUILD_VFY="cd backend && pnpm test && pnpm run typecheck && pnpm run build" LINT1="cd backend && pnpm test 2>&1 | tail -10" LINT2="cd backend && pnpm run typecheck 2>&1 | tail -10" LINT3="cd backend && pnpm run build 2>&1 | tail -10" ;; learning_ai_local_llms) NAME="Local LLM Lab"; ID="localllmlab" STACK="Next.js 16 (dashboard) + Ollama + @bytelyst/llm-router + Python (TTS)" BUILD_VFY="cd dashboard && pnpm test && pnpm typecheck && pnpm build" LINT1="cd dashboard && pnpm test 2>&1 | tail -10" LINT2="cd dashboard && pnpm typecheck 2>&1 | tail -10" LINT3="cd dashboard && pnpm build 2>&1 | tail -10" ;; learning_ai_productivity_web) NAME="Productivity Web"; ID="(internal)" STACK="Next.js 15 + React 19 + TailwindCSS" BUILD_VFY="npm run typecheck && npm run build" LINT1="npm run typecheck 2>&1 | tail -10" LINT2="npm run build 2>&1 | tail -10" ;; learning_ai_talk2obsidian) NAME="Talk2Obs"; ID="talk2obs" STACK="Fastify 5 + TypeScript ESM (backend) + Vite + React 19 (web) + SQLite + Ollama + whisper-cpp" BUILD_VFY="cd backend && npm test && npm run typecheck" LINT1="cd backend && npm test 2>&1 | tail -10" LINT2="cd backend && npm run typecheck 2>&1 | tail -10" AIDER_READ2="docs/ROADMAP.md" ;; learning_ai_mac_tooling) NAME="Mac Tooling"; ID="(internal)" STACK="Python (FastAPI backend) + Vite + React (dashboard) + SQLite + Swift (menu bar)" BUILD_VFY="cd dashboard && npm run build" LINT1="cd dashboard && npm run build 2>&1 | tail -10" ;; oss/learning_ai_claw-code-oss) NAME="Claw Code OSS"; ID="(upstream)" STACK="Rust (workspace) + Python + TypeScript" BUILD_VFY="cargo build --workspace && cargo test --workspace" LINT1="cargo fmt --check 2>&1 | tail -10" LINT2="cargo clippy --workspace --all-targets -- -D warnings 2>&1 | tail -10" LINT3="cargo test --workspace 2>&1 | tail -10" ;; oss/learning_ai_claw-cowork) NAME="Claw Cowork"; ID="clawcowork" STACK="Rust (workspace) + React/TypeScript (Tauri frontend) + Python (skills server)" BUILD_VFY="cargo build --workspace && cargo test --workspace" LINT1="cargo fmt --check 2>&1 | tail -10" LINT2="cargo clippy --workspace --all-targets -- -D warnings 2>&1 | tail -10" LINT3="cargo test --workspace 2>&1 | tail -10" AIDER_READ2="COWORK.md" ;; *) warn "Unknown repo: $1 — using minimal defaults" NAME="$1"; ID="(unknown)" STACK="(see AGENTS.md)" BUILD_VFY="(see AGENTS.md)" ;; esac } # Compute relative path from a repo to the canonical guidelines file. # Repos at workspace root (depth 0) → ../learning_ai_common_plat/... # Repos one level deep (e.g. oss/foo, depth 1) → ../../learning_ai_common_plat/... # common_plat itself → AI.dev/SKILLS/... canonical_path_for() { local repo="$1" if [[ "$repo" == "learning_ai_common_plat" ]]; then echo "${CANONICAL_REL_FROM_COMMON_PLAT}" return fi local slashes="${repo//[^\/]/}" local depth=${#slashes} local prefix="" for ((i=0; i<=depth; i++)); do prefix+="../" done echo "${prefix}learning_ai_common_plat/${CANONICAL_REL_FROM_COMMON_PLAT}" } # ── file generators ───────────────────────────────────────────────────────── write_editorconfig() { cat > "$1" << 'EOF' root = true [*] indent_style = space indent_size = 2 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.md] trim_trailing_whitespace = false EOF } write_aider_conf() { local dest="$1" local lint_block="" for cmd in "${LINT1:-}" "${LINT2:-}" "${LINT3:-}"; do [[ -z "$cmd" ]] && continue lint_block+=" - '${cmd}'"$'\n' done local read_block=" - AGENTS.md"$'\n' [[ -n "${AIDER_READ2:-}" ]] && read_block+=" - ${AIDER_READ2}"$'\n' cat > "$dest" << EOF # .aider.conf.yml — Aider Configuration for ${NAME} # Auto-generated by learning_ai_common_plat/scripts/update-agent-docs.sh # Hand-edit will be overwritten on next run. read: ${read_block} conventions: AGENTS.md lint-cmd: ${lint_block} auto-commits: false EOF } write_copilot_pointer() { local dest_dir="$1" canonical="$2" mkdir -p "$dest_dir" cat > "${dest_dir}/copilot-instructions.md" << EOF # GitHub Copilot Instructions — ${NAME} **Project:** ${NAME} — \`${ID}\` **Stack:** ${STACK} ## Read these (in order) before suggesting code 1. **\`AGENTS.md\`** at the repo root — repo-specific layout, conventions, build commands, file ownership. 2. **\`${canonical}\`** — ecosystem-wide agent behavior rules (Karpathy + ByteLyst). Identical across every repo in the workspace. Both files together are the complete contract. This pointer file contains no rules of its own to avoid duplication and drift. ## Quick verification command \`\`\`bash ${BUILD_VFY} \`\`\` EOF } # Idempotently prepend a "Read first" pointer block to AGENTS.md. # The block is delimited by HTML comment markers so we can re-find and # replace it without touching the rest of AGENTS.md. # # Note: awk's -v does not preserve literal newlines reliably, so the # replacement block is written to a temp file and slurped via getline. prepend_agents_pointer() { local agents_md="$1" canonical="$2" [[ ! -f "$agents_md" ]] && return 0 local begin_marker="" local end_marker="" local tmp_block tmp_block="$(mktemp)" cat > "$tmp_block" << EOF ${begin_marker} > **Read first (ecosystem-wide agent behavior):** > [\`${canonical}\`](${canonical}) > > The link above is the single source of truth for agent behavior across every > ByteLyst repo (Karpathy + ByteLyst rules: tests sacred, verify before done, > no shared-infra hand-edits, no \`console.log\`/\`print\`, productId on every > Cosmos doc, conventional commits, style preservation). > > The per-repo content below extends — never duplicates — the canonical rules. ${end_marker} EOF if grep -qF "$begin_marker" "$agents_md"; then # Replace existing block in place. awk -v begin="$begin_marker" -v endm="$end_marker" -v block_file="$tmp_block" ' BEGIN { while ((getline line < block_file) > 0) { new = (new == "" ? line : new "\n" line) } close(block_file) in_block = 0 } $0 == begin { print new; in_block = 1; next } in_block && $0 == endm { in_block = 0; next } !in_block { print } ' "$agents_md" > "${agents_md}.tmp" && mv "${agents_md}.tmp" "$agents_md" else # Insert after first H1 (line beginning with "# "). If no H1, prepend. if grep -qE '^# ' "$agents_md"; then awk -v block_file="$tmp_block" ' BEGIN { while ((getline line < block_file) > 0) { block = (block == "" ? line : block "\n" line) } close(block_file) inserted = 0 } { print } !inserted && /^# / { print ""; print block; inserted = 1 } ' "$agents_md" > "${agents_md}.tmp" && mv "${agents_md}.tmp" "$agents_md" else { cat "$tmp_block"; echo ""; cat "$agents_md"; } > "${agents_md}.tmp" \ && mv "${agents_md}.tmp" "$agents_md" fi fi rm -f "$tmp_block" } # Delete legacy agent files (replaced by the canonical pointer architecture). delete_legacy_files() { local repo_dir="$1" local deleted=() for f in .cursorrules .windsurfrules .clinerules CLAUDE.md; do local target="${repo_dir}/${f}" [[ ! -e "$target" ]] && continue if git -C "$repo_dir" ls-files --error-unmatch "$f" >/dev/null 2>&1; then git -C "$repo_dir" rm -q "$f" 2>/dev/null || rm -f "$target" else rm -f "$target" fi deleted+=("$f") done if [[ ${#deleted[@]} -gt 0 ]]; then ok "deleted legacy: ${deleted[*]}" fi } # ── main loop ─────────────────────────────────────────────────────────────── if [[ ! -f "$REPOS_TXT" ]]; then err "repos.txt not found at: ${REPOS_TXT}" exit 1 fi REPOS=() while IFS= read -r line; do [[ "$line" =~ ^[[:space:]]*# ]] && continue [[ -z "${line// }" ]] && continue REPOS+=("$line") done < "$REPOS_TXT" info "Base directory: ${BASE_DIR}" info "Repos to process: ${#REPOS[@]}" $DRY_RUN && warn "DRY RUN — no files will be written or committed" $NO_COMMIT && info "NO-COMMIT mode — files will be written but not committed" for REPO in "${REPOS[@]}"; do header "$REPO" REPO_DIR="${BASE_DIR}/${REPO}" if [[ ! -d "$REPO_DIR" ]]; then warn "Directory not found at ${REPO_DIR} — skipping" continue fi set_meta "$REPO" CANONICAL_PATH="$(canonical_path_for "$REPO")" info "canonical pointer → ${CANONICAL_PATH}" if [[ ! -f "${REPO_DIR}/AGENTS.md" ]]; then warn "AGENTS.md not found in ${REPO} — skipping (bootstrap AGENTS.md manually first)" continue fi if $DRY_RUN; then info "Would: delete legacy files, rewrite copilot pointer, prepend AGENTS.md pointer" info " regenerate .editorconfig and .aider.conf.yml" continue fi # 1. Delete legacy files (.cursorrules, .windsurfrules, .clinerules, CLAUDE.md) delete_legacy_files "$REPO_DIR" # 2. Regenerate small config files write_editorconfig "${REPO_DIR}/.editorconfig" ok ".editorconfig" write_aider_conf "${REPO_DIR}/.aider.conf.yml" ok ".aider.conf.yml" # 3. Rewrite copilot pointer (thin, no rules) write_copilot_pointer "${REPO_DIR}/.github" "$CANONICAL_PATH" ok ".github/copilot-instructions.md (pointer)" # 4. Idempotently prepend canonical pointer to AGENTS.md prepend_agents_pointer "${REPO_DIR}/AGENTS.md" "$CANONICAL_PATH" ok "AGENTS.md (pointer block ensured)" # 5. Commit if there are any changes in this repo if $NO_COMMIT; then info "Skipping commit (--no-commit)" continue fi if git -C "$REPO_DIR" status --porcelain 2>/dev/null | grep -q .; then # Stage only paths that currently exist (legacy file deletes are already # staged by `git rm` inside delete_legacy_files()). for f in .editorconfig .aider.conf.yml AGENTS.md .github/copilot-instructions.md; do [[ -e "${REPO_DIR}/${f}" ]] && git -C "$REPO_DIR" add "$f" 2>/dev/null || true done if git -C "$REPO_DIR" commit -m "chore(docs): consolidate agent docs to single source of truth - Delete legacy .cursorrules, .windsurfrules, .clinerules, CLAUDE.md (content duplicated AGENTS.md; replaced by canonical pointer architecture) - Rewrite .github/copilot-instructions.md as thin pointer to AGENTS.md and the canonical agent-behavior-guidelines.md - Prepend canonical 'Read first' pointer block to AGENTS.md - Regenerate .editorconfig and .aider.conf.yml Canonical source: learning_ai_common_plat/AI.dev/SKILLS/agent-behavior-guidelines.md Managed by: learning_ai_common_plat/scripts/update-agent-docs.sh" \ --no-verify 2>/dev/null; then ok "Committed changes in ${REPO}" CHANGED_REPOS+=("$REPO") else warn "Detected changes in ${REPO}, but no commit was created" fi else info "No changes in ${REPO}" fi done # ── summary ────────────────────────────────────────────────────────────────── echo "" if [[ ${#CHANGED_REPOS[@]} -gt 0 ]]; then ok "Updated and committed ${#CHANGED_REPOS[@]} repo(s):" for r in "${CHANGED_REPOS[@]}"; do echo " • $r" done echo "" info "Push when ready:" for r in "${CHANGED_REPOS[@]}"; do echo " git -C ${BASE_DIR}/${r} push" done else if $NO_COMMIT; then ok "Files regenerated. No commits made (--no-commit). Inspect changes with: git -C status" elif $DRY_RUN; then ok "Dry run complete. Re-run without --dry-run to apply." else ok "All repos already in sync — no changes." fi fi