#!/usr/bin/env bash set -euo pipefail # repo-bootstrap.sh — Repository Intelligence Bootstrap for the dev_agent_team # # Generates/maintains a repository-specific `.opencode/` knowledge layer: # .opencode/AGENTS.md repository intelligence entry point # .opencode/.bootstrap-meta staleness metadata (key=value, no JSON parser needed) # .opencode/skills//SKILL.md repository-specific skills (opencode skill format) # plus a root AGENTS.md pointer (opencode auto-loads root AGENTS.md from the project root). # # The tool is GENERIC: it never hardcodes any repository-specific assumption. It # only scaffolds structure and records mechanically detectable signals (top-level # layout, build/deploy manifest presence). Semantic enrichment is done by agents # (Orchestrator/Explorer/Architect/Builder/Tester/...) after bootstrap runs. # # Usage: # repo-bootstrap.sh status → print freshness verdict. exit 0=fresh, 1=stale/missing, 2=error # repo-bootstrap.sh bootstrap → create/refresh generated intelligence (idempotent) # repo-bootstrap.sh refresh → force-regenerate GENERATED files (same as bootstrap --force) # # Idempotency / preservation rules (binding): # * A generated file carries a marker: first line `` # (AGENTS.md) or `generated-by` metadata (SKILL.md frontmatter). The tool ONLY # rewrites files it generated. # * Files WITHOUT the marker are treated as MANUAL — never touched, never removed. # Agents that substantially enrich a generated file should strip its marker to # freeze it as manual. # * `bootstrap` rewrites a generated file only when it is missing or the staleness # fingerprint changed. A second run in an unchanged repo is a no-op (idempotent). # * Existing `.opencode/AGENTS.md`/skills that are manual are left alone and reported. VERSION=1.0 MARKER_COMMENT="" usage() { sed -n '2,40p' "$0" | sed 's/^# \{0,1\}//' exit 2 } # --- helpers --------------------------------------------------------------- reporoot() { # Prefer git worktree root; fall back to cwd (script is safe when run anywhere inside). if command -v git >/dev/null 2>&1; then local root if root="$(git rev-parse --show-toplevel 2>/dev/null)"; then printf '%s' "$root" return 0 fi fi pwd -P } sha256_of() { # sha256_of if command -v sha256sum >/dev/null 2>&1; then printf '%s' "$1" | sha256sum | cut -d' ' -f1 elif command -v cksum >/dev/null 2>&1; then printf '%s' "$1" | cksum | awk '{print $1}' else printf '%s' "${#1}" fi } top_level_snapshot() { # top_level_snapshot -> newline list, sorted find "$1" -maxdepth 1 -mindepth 1 \( -name .git -o -name node_modules -o -name target -o -name build -o -name dist \) -prune -o -maxdepth 1 -mindepth 1 -printf '%f\n' 2>/dev/null | LC_ALL=C sort } # detect_manifest : 0 if any of the files exist at root detect_manifest() { local root="$1"; shift local f for f in "$@"; do [ -f "$root/$f" ] && return 0 done return 1 } # detect_any_dir : 0 if any of the dirs exist at root detect_any_dir() { local root="$1"; shift local d for d in "$@"; do [ -d "$root/$d" ] && return 0 done return 1 } # has_workflow_files has_workflow_files() { [ -d "$1/.github/workflows" ] && ls "$1/.github/workflows"/* >/dev/null 2>&1 } # --- signals --------------------------------------------------------------- detect_signals() { # detect_signals -> sets BUILD_IND / DEPLOY_IND / CODE_IND local root="$1" BUILD_IND=0; DEPLOY_IND=0; CODE_IND=0 if detect_manifest "$root" \ package.json pnpm-lock.yaml yarn.lock package-lock.json \ pyproject.toml requirements.txt setup.py \ Cargo.toml go.mod pom.xml build.gradle build.gradle.kts \ Makefile CMakeLists.txt Gemfile composer.json mix.exs \ pubspec.yaml meson.build WORKSPACE BUILD dune-project \ *.csproj *.sln; then BUILD_IND=1 fi if detect_manifest "$root" \ Dockerfile docker-compose.yml docker-compose.yaml \ .gitlab-ci.yml Jenkinsfile cloudbuild.yaml heroku.yml \ fly.toml vercel.json netlify.toml \ *.tf *.tfvars || has_workflow_files "$root" || detect_any_dir "$root" k8s helm terraform systemd; then DEPLOY_IND=1 fi if [ "$BUILD_IND" -eq 1 ] || detect_any_dir "$root" src lib packages cmd internal app core; then CODE_IND=1 fi } # --- meta ------------------------------------------------------------------ META_FILE=".bootstrap-meta" META_KEYS="schema tool version generated_at git_head manifest_fingerprint top_level_fingerprint signals" read_meta_value() { # read_meta_value local file="$1" key="$2" [ -f "$file" ] || return 1 sed -n "s/^${key}=//p" "$file" | head -n1 } # --- fingerprint ----------------------------------------------------------- current_fingerprint() { # current_fingerprint -> sets MANIFEST_FP / TOPLEVEL_FP / GIT_HEAD / SIGNAL_TXT local root="$1" GIT_HEAD="" if command -v git >/dev/null 2>&1; then GIT_HEAD="$(git -C "$root" rev-parse --short HEAD 2>/dev/null || true)" fi local manifest_lines="" f for f in package.json pnpm-lock.yaml yarn.lock package-lock.json pyproject.toml requirements.txt setup.py Cargo.toml go.mod pom.xml build.gradle build.gradle.kts Makefile CMakeLists.txt Gemfile composer.json mix.exs pubspec.yaml meson.build WORKSPACE BUILD dune-project Dockerfile docker-compose.yml docker-compose.yaml .gitlab-ci.yml Jenkinsfile cloudbuild.yaml heroku.yml fly.toml vercel.json netlify.toml; do if [ -f "$root/$f" ]; then manifest_lines+="$(printf '%s' "$f:$(sha256_of "$(cat "$root/$f")")"; printf '\n')" fi done MANIFEST_FP="$(sha256_of "$manifest_lines")" TOPLEVEL_FP="$(sha256_of "$(top_level_snapshot "$root")")" SIGNAL_TXT="build=$BUILD_IND deploy=$DEPLOY_IND code=$CODE_IND" } # --- verdict --------------------------------------------------------------- verdict() { # verdict -> sets VERDICT=fresh|stale|missing|error ; sets META_OUT local root="$1" local dir="$root/.opencode" VERDICT="error"; META_OUT="" [ -d "$root" ] || return 2 if [ ! -d "$dir" ]; then VERDICT="missing"; return 0 fi if [ ! -f "$dir/$META_FILE" ]; then VERDICT="stale"; return 0 fi local schema tool_version meta_fp mt_fp signals schema="$(read_meta_value "$dir/$META_FILE" schema)" tool_version="$(read_meta_value "$dir/$META_FILE" version)" meta_fp="$(read_meta_value "$dir/$META_FILE" manifest_fingerprint)" mt_fp="$(read_meta_value "$dir/$META_FILE" top_level_fingerprint)" signals="$(read_meta_value "$dir/$META_FILE" signals)" detect_signals "$root" current_fingerprint "$root" if [ "$schema" != "1" ] || [ "$tool_version" != "$VERSION" ] \ || [ "$meta_fp" != "$MANIFEST_FP" ] || [ "$mt_fp" != "$TOPLEVEL_FP" ] \ || [ "$signals" != "$SIGNAL_TXT" ]; then VERDICT="stale"; return 0 fi VERDICT="fresh"; return 0 } # --- generation ------------------------------------------------------------ write_if_owned() { # write_if_owned ; sets CREATED/UPDATED/SKIPPED counters local path="$1" content="$2" force="$3" if [ ! -e "$path" ]; then mkdir -p "$(dirname "$path")" printf '%s' "$content" > "$path" CREATED=$((CREATED + 1)) elif grep -qF "GENERATED-SCAFFOLD dev_agent_team" "$path" || grep -qF "generated-by: dev_agent_team-repo-bootstrap" "$path"; then if [ "$force" = "1" ] || [ "$VERDICT" = "stale" ] || [ "$VERDICT" = "missing" ]; then printf '%s' "$content" > "$path" UPDATED=$((UPDATED + 1)) else SKIPPED=$((SKIPPED + 1)) # fresh and generated → leave as-is (idempotent) fi else MANUAL=$((MANUAL + 1)) # manual content — never touch fi } root_agents_md() { # root_agents_md printf '%s\n' \ "$MARKER_COMMENT" \ "# Repository Intelligence" \ "" \ "Read \`.opencode/AGENTS.md\` for repository-specific context before starting work." \ "Deeper knowledge lives in \`.opencode/skills/\` (repo-context, architecture, build-and-test, conventions, deployment)." \ "" \ "Treat repository intelligence as context, not absolute truth: verify against the actual repository when they disagree." \ "" } repo_agents_md() { # repo_agents_md local rname; rname="$(basename "$1")" local bt="" arch="" dep="" conv="# Conventions"$'\n\n# Repository coding, naming, structure, and workflow conventions are maintained in \`.opencode/skills/conventions/SKILL.md\`.\n' [ "$BUILD_IND" -eq 1 ] && bt="# Build & Test"$'\n\n# Build/test/lint/validation commands live in \`.opencode/skills/build-and-test/SKILL.md\`.\n' [ "$CODE_IND" -eq 1 ] && arch="# Architecture"$'\n\n# Components, relationships, boundaries, and architectural rules live in \`.opencode/skills/architecture/SKILL.md\`.\n' [ "$DEPLOY_IND" -eq 1 ] && dep="# Deployment"$'\n\n# Deployment and runtime information lives in \`.opencode/skills/deployment/SKILL.md\`.\n' printf '%s\n' \ "$MARKER_COMMENT" \ "# AGENTS.md — Repository Intelligence for \`${rname}\`" \ "" \ "## What this repository is" \ "" \ "- (fill in: purpose, for whom, why it exists; verified by Philosopher/Explorer)" \ "" \ "## How it is structured" \ "" \ "- (fill in: top-level layout and module roles; see \`.opencode/skills/repo-context/SKILL.md\`)" \ "" \ "## Build & test" \ "" \ "$bt" \ "## Conventions" \ "" \ "$conv" \ "## Architecture constraints" \ "" \ "$arch" \ "## Deployment / runtime" \ "" \ "$dep" \ "## Rules for agents (binding)" \ "" \ "- Consult \`.opencode/\` before making architectural or implementation decisions." \ "- Treat this knowledge as context, not absolute truth; verify against the source of truth." \ "- Owned skills: Explorer→repo-context, Architect→architecture, Builder/Tester→build-and-test, Maintainer→cleanup/consistency, Reviewer→validation." \ "- Never fill \`.opencode/\` with task-specific noise; add only durable knowledge." \ "" \ "## Bootstrap metadata" \ "" \ "- Generated by dev_agent_team repo-bootstrap v${VERSION} on $(date -u +%Y-%m-%dT%H:%M:%SZ)." \ "" } skill_meta() { printf '%s\n' "---" "name: $1" "description: $2" "metadata:" " generated-by: dev_agent_team-repo-bootstrap" " bootstrap-version: \"$VERSION\"" "---" ""; } skill_repo_context() { skill_meta repo-context \ "High-level orientation for a repository: purpose, structure, modules, dependencies, and entry points. Use when entering an unfamiliar repository or before architectural and implementation work." printf '%s\n' \ "# Repo Context" \ "" \ "> Generated scaffold by dev_agent_team repo-bootstrap v${VERSION}. OWNER: Explorer (verify/enrich). Treat as context — verify against the repository." \ "" \ "## Purpose" \ "- (What does this repository do? For whom? Why?)" \ "" \ "## Structure" \ "- (Top-level directories and their roles)" \ "" \ "## Important modules / entry points" \ "- (Files/dirs that matter most for common work)" \ "" \ "## Dependencies (bootstrap-detected — verify)" \ "- (manifest files and package manager)" \ "" \ "## Runtime requirements" \ "- (verify)" \ "" \ "## Generated vs handwritten files" \ "- (verify)" \ "" \ "## Dangerous areas / fragile components" \ "- (verify)" \ "" \ "## Known technical decisions" \ "- (verify/add reference to ADRs)" \ "" } skill_architecture() { skill_meta architecture \ "Repository architecture: components, relationships, boundaries, and architectural rules. Use when making architectural decisions or understanding system design." printf '%s\n' \ "# Architecture" \ "" \ "> Generated scaffold by dev_agent_team repo-bootstrap v${VERSION}. OWNER: Architect (verify/enrich). Treat as context — verify against the repository." \ "" \ "## Components & modules" \ "- (list and their relationships)" \ "" \ "## Boundaries & ownership" \ "- (layering, module boundaries, responsibilities)" \ "" \ "## Architectural rules & constraints" \ "- (patterns that must be followed)" \ "" \ "## Integration points" \ "- (external systems, APIs, data flows)" \ "" \ "## Known architectural decisions" \ "- (ADR summaries/links)" \ "" } skill_build_test() { skill_meta build-and-test \ "How to build, test, lint, type-check, and validate a repository. Use before modifying code or when running or verifying project commands." printf '%s\n' \ "# Build & Test" \ "" \ "> Generated scaffold by dev_agent_team repo-bootstrap v${VERSION}. OWNERS: Builder + Tester (verify/enrich). Treat as context — verify against the repository." \ "" \ "## Framework / toolchain (bootstrap-detected — verify)" \ "- (manifest files seen during bootstrap; confirm actual toolchain)" \ "" \ "## Build commands" \ "- (list exact commands and order)" \ "" \ "## Test commands" \ "- (list exact commands; note the fast subset for quick checks)" \ "" \ "## Lint / type-check / static analysis" \ "- (list exact commands)" \ "" \ "## Validation order" \ "- (which commands, in which order, before declaring work done)" \ "" \ "## Known build/test gotchas" \ "- (verify/add)" \ "" } skill_conventions() { skill_meta conventions \ "Repository-specific coding, naming, structure, and workflow conventions. Use when writing code or documentation in this repository." printf '%s\n' \ "# Conventions" \ "" \ "> Generated scaffold by dev_agent_team repo-bootstrap v${VERSION}. OWNER: Maintainer (cleanup/consistency); every agent follows conventions. Treat as context — verify against the repository." \ "" \ "## Language / style" \ "- (fill)" \ "" \ "## Naming" \ "- (fill)" \ "" \ "## Structure / layout" \ "- (fill)" \ "" \ "## Workflow (branching, commits, review)" \ "- (fill)" \ "" \ "## Constraints / gotchas" \ "- (fill)" \ "" } skill_deployment() { skill_meta deployment \ "Deployment and runtime information for a repository, including environments, packaging, and operations. Use when deploying, operating, or debugging runtime behavior." printf '%s\n' \ "# Deployment" \ "" \ "> Generated scaffold by dev_agent_team repo-bootstrap v${VERSION}. Treat as context — verify against the repository." \ "" \ "## Deployment targets / environments (bootstrap-detected — verify)" \ "- (indicators: Dockerfile, compose, workflows, IaC files — confirm actual setup)" \ "" \ "## Build / packaging for deploy" \ "- (fill)" \ "" \ "## Runtime requirements" \ "- (fill)" \ "" \ "## Operations / rollout / rollback" \ "- (fill)" \ "" \ "## Monitoring / logs / troubleshooting" \ "- (fill)" \ "" } do_write_skills() { # do_write_skills local root="$1" force="$2" skills="$root/.opencode/skills" write_if_owned "$skills/repo-context/SKILL.md" "$(skill_repo_context)" "$force" write_if_owned "$skills/conventions/SKILL.md" "$(skill_conventions)" "$force" if [ "$CODE_IND" -eq 1 ]; then write_if_owned "$skills/architecture/SKILL.md" "$(skill_architecture)" "$force" fi if [ "$BUILD_IND" -eq 1 ]; then write_if_owned "$skills/build-and-test/SKILL.md" "$(skill_build_test)" "$force" fi if [ "$DEPLOY_IND" -eq 1 ]; then write_if_owned "$skills/deployment/SKILL.md" "$(skill_deployment)" "$force" fi } write_meta() { # write_meta local root="$1" mkdir -p "$root/.opencode" current_fingerprint "$root" # Compare only the stable fields (excluding generated_at) so a second run in an # unchanged repo is a true no-op — even across a second boundary — and the # original timestamp is preserved. Without this, idempotency flakes whenever # two runs straddle a second boundary (generated_at would otherwise differ). local stable stable="$(printf '%s\n' \ "schema=1" \ "tool=dev_agent_team-repo-bootstrap" \ "version=$VERSION" \ "git_head=$GIT_HEAD" \ "manifest_fingerprint=$MANIFEST_FP" \ "top_level_fingerprint=$TOPLEVEL_FP" \ "signals=$SIGNAL_TXT")" local existing="" existing_stable="" [ -f "$root/.opencode/$META_FILE" ] && existing="$(cat "$root/.opencode/$META_FILE")" [ -n "$existing" ] && existing_stable="$(printf '%s\n' "$existing" | grep -v '^generated_at=' || true)" if [ "$existing_stable" = "$stable" ]; then return 0 fi local new_meta new_meta="$(printf '%s\n' "$stable" \ "generated_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)")" printf '%s\n' "$new_meta" > "$root/.opencode/$META_FILE" } # --- commands -------------------------------------------------------------- cmd_status() { local root="$1" detect_signals "$root" verdict "$root" case "$VERDICT" in fresh) echo "REPO_BOOTSTRAP_STATUS: fresh # .opencode up to date (root: $root)"; return 0 ;; stale) echo "REPO_BOOTSTRAP_STATUS: stale # signals changed or metadata outdated (root: $root)"; return 1 ;; missing) echo "REPO_BOOTSTRAP_STATUS: missing # no .opencode intelligence (root: $root)"; return 1 ;; *) echo "REPO_BOOTSTRAP_STATUS: error # cannot evaluate (root: $root)" >&2; return 2 ;; esac } cmd_bootstrap() { local root="$1" force="$2" CREATED=0; UPDATED=0; SKIPPED=0; MANUAL=0 detect_signals "$root" verdict "$root" [ "$VERDICT" = "error" ] && { echo "ERROR: cannot evaluate repository root: $root" >&2; return 2; } echo "==> Repository Intelligence bootstrap (root: $root)" echo "==> Signals: build=$BUILD_IND deploy=$DEPLOY_IND code=$CODE_IND" echo "==> State before: $VERDICT" # Root AGENTS.md pointer: only when absent or generated-owned (never clobber manual). write_if_owned "$root/AGENTS.md" "$(root_agents_md "$root")" "$force" # .opencode/AGENTS.md + skills. write_if_owned "$root/.opencode/AGENTS.md" "$(repo_agents_md "$root")" "$force" do_write_skills "$root" "$force" # Meta last, so it reflects what was just produced. write_meta "$root" echo "==> created=$CREATED updated=$UPDATED skipped-fresh=$SKIPPED manual-preserved=$MANUAL" if [ "$MANUAL" -gt 0 ]; then echo "==> NOTE: $MANUAL file(s) treated as manual content and preserved untouched." fi echo "==> Next: agents should READ .opencode/AGENTS.md and relevant skills, verify facts against the repository, and enrich owned files (stripping the generated marker when freezing them as manual)." return 0 } # --- main ------------------------------------------------------------------- [ $# -ge 1 ] || usage CMD="$1"; shift || true ROOT="" while [ $# -gt 0 ]; do case "$1" in --root) ROOT="$2"; shift 2 ;; *) break ;; esac done [ -z "$ROOT" ] && ROOT="$(reporoot)" case "$CMD" in status) cmd_status "$ROOT" ;; bootstrap) cmd_bootstrap "$ROOT" 0 ;; refresh) cmd_bootstrap "$ROOT" 1 ;; -h|--help) usage ;; *) echo "ERROR: unknown command: $CMD" >&2; usage ;; esac