clone

a lazygit-style terminal viewer for openspec repos: watch changes, tasks and spec deltas land as agents work

main
lazyopenspec / scripts / swarm-sim.sh
swarm-sim.sh 6.1K executable
1#!/usr/bin/env bash2# Simulates a small swarm of agents working through OpenSpec changes so you can3# watch lazyopenspec react. Works with macOS bash 3.2 and BSD tools.4#5# scripts/swarm-sim.sh [target-dir] (default /tmp/lazyopenspec-demo)6# go run . [target-dir] --follow (in another pane)7set -euo pipefail89here="$(cd "$(dirname "$0")/.." && pwd)"10target="${1:-/tmp/lazyopenspec-demo}"11agents="${AGENTS:-3}"12changes="$target/openspec/changes"13topics="add-recurring-tasks:task-management add-api-tokens:user-auth add-audit-log:audit-log add-bulk-edit:task-management improve-search-ranking:search add-slack-alerts:notifications"1415rm -rf "$target"16mkdir -p "$target"17cp -Rp "$here/testdata/fixture/." "$target/"18git -C "$target" init -q19git -C "$target" add -A20git -C "$target" -c user.name=sim -c user.email=sim@example.com commit -qm "baseline"2122echo "swarm-sim: $agents agents working in $target (ctrl+c to stop)"23echo "watch with: go run $here $target --follow"24echo2526say() { printf '%s [agent-%s] %s\n' "$(date +%H:%M:%S)" "$1" "$2"; }2728# Atomic write, the way editors and agents save: write a temp file, then rename.29atomic() { # atomic <file> (content on stdin)30 local tmp31 tmp="$(dirname "$1")/.$(basename "$1").$$.$RANDOM.tmp"32 cat >"$tmp"33 mv -f "$tmp" "$1"34}3536active() { find "$changes" -mindepth 1 -maxdepth 1 -type d ! -name archive 2>/dev/null; }37pick() { awk -v r="$RANDOM" 'NF { a[n++] = $0 } END { if (n) print a[r % n] }'; }3839check_task() { # check off the next unchecked top-level task somewhere40 local f41 f="$(grep -l '^- \[ \]' $(active | sed 's|$|/tasks.md|') 2>/dev/null | pick || true)"42 [ -n "$f" ] || return 143 local task44 task="$(grep -m1 '^- \[ \]' "$f" | sed 's/^- \[ \] //' | cut -c1-50)"45 awk '!done && /^- \[ \]/ { sub(/\[ \]/, "[x]"); done = 1 } { print }' "$f" | atomic "$f"46 say "$1" "checked \"$task\" in $(basename "$(dirname "$f")")"47}4849add_requirement() { # append an ADDED requirement to some delta spec50 local f n51 f="$(active | while read -r d; do ls "$d"/specs/*/spec.md 2>/dev/null; done | pick || true)"52 [ -n "$f" ] || return 153 n=$((RANDOM % 900 + 100))54 if ! grep -q '^## ADDED Requirements' "$f"; then printf '\n## ADDED Requirements\n' >>"$f"; fi55 awk -v n="$n" '56 { print }57 /^## ADDED Requirements/ {58 print ""59 print "### Requirement: Generated behavior " n60 print "The system SHALL handle case " n " consistently."61 print ""62 print "#### Scenario: Case " n63 print "- **WHEN** case " n " occurs"64 print "- **THEN** the system handles it"65 }' "$f" | atomic "$f"66 say "$1" "added requirement $n to $(echo "$f" | sed "s|$changes/||")"67}6869advance_change() { # move one simulated change to its next artifact70 local entry name cap dir71 entry="$(echo $topics | tr ' ' '\n' | pick)"72 name="${entry%%:*}" cap="${entry##*:}" dir="$changes/$name"73 if [ ! -d "$dir" ]; then74 [ -d "$changes/archive" ] && ls "$changes/archive" | grep -q -- "-$name\$" && return 175 mkdir -p "$dir"76 printf 'schema: spec-driven\ncreated: %s\n' "$(date +%F)" | atomic "$dir/.openspec.yaml"77 say "$1" "openspec new change $name"78 elif [ ! -f "$dir/proposal.md" ]; then79 printf '## Why\n\nUsers keep asking for %s; doing it by hand is slow and error-prone.\n\n## What Changes\n\n- Introduce %s\n- Update the %s capability\n\n## Impact\n\n- Affected specs: %s\n' \80 "${name//-/ }" "${name//-/ }" "$cap" "$cap" | atomic "$dir/proposal.md"81 say "$1" "wrote proposal for $name"82 elif [ ! -d "$dir/specs" ]; then83 mkdir -p "$dir/specs/$cap"84 printf '## ADDED Requirements\n\n### Requirement: %s\nThe system SHALL support %s.\n\n#### Scenario: Happy path\n- **WHEN** a member uses %s\n- **THEN** it works\n' \85 "${name//-/ }" "${name//-/ }" "${name//-/ }" | atomic "$dir/specs/$cap/spec.md"86 say "$1" "drafted spec delta $name/specs/$cap"87 elif [ ! -f "$dir/design.md" ] && [ $((RANDOM % 2)) -eq 0 ]; then88 printf '## Context\n\nSketching %s.\n\n## Decisions\n\n- Keep it behind a feature flag\n\n## Open Questions\n\n- Rollout order?\n' "$name" | atomic "$dir/design.md"89 say "$1" "wrote design for $name"90 elif [ ! -f "$dir/tasks.md" ]; then91 {92 printf '## 1. Implementation\n\n'93 for i in 1 2 3 4 5; do printf -- '- [ ] 1.%s Step %s of %s\n' "$i" "$i" "$name"; done94 } | atomic "$dir/tasks.md"95 say "$1" "planned tasks for $name"96 else97 return 198 fi99}100101edit_design() {102 local f103 f="$(active | sed 's|$|/design.md|' | while read -r p; do [ -f "$p" ] && echo "$p"; done | pick || true)"104 [ -n "$f" ] || return 1105 printf -- '- Note from agent-%s at %s\n' "$1" "$(date +%T)" >>"$f"106 say "$1" "noted a decision in $(basename "$(dirname "$f")")/design.md"107}108109archive_done() { # archive a change whose tasks are all checked110 local d name f cap main111 d="$(active | while read -r c; do112 [ -f "$c/tasks.md" ] && grep -q '^- \[x\]' "$c/tasks.md" && ! grep -q '^- \[ \]' "$c/tasks.md" && echo "$c"113 done | pick || true)"114 [ -n "$d" ] || return 1115 name="$(basename "$d")"116 for f in "$d"/specs/*/spec.md; do117 [ -f "$f" ] || continue118 cap="$(basename "$(dirname "$f")")" main="$target/openspec/specs/$cap/spec.md"119 mkdir -p "$(dirname "$main")"120 [ -f "$main" ] || printf '# %s Specification\n\n## Purpose\nTBD - created by archiving change %s.\n\n## Requirements\n' "$cap" "$name" >"$main"121 awk '/^## ADDED Requirements/ { on = 1; next } /^## / { on = 0 } on' "$f" >>"$main"122 done123 mkdir -p "$changes/archive"124 mv "$d" "$changes/archive/$(date +%F)-$name"125 say "$1" "archived $name"126}127128agent() {129 local id="$1"130 while true; do131 sleep "$(awk -v r="$RANDOM" 'BEGIN { printf "%.1f", 0.5 + (r % 26) / 10 }')"132 case $((RANDOM % 10)) in133 0 | 1 | 2 | 3) check_task "$id" || advance_change "$id" || true ;;134 4 | 5) advance_change "$id" || check_task "$id" || true ;;135 6) add_requirement "$id" || true ;;136 7) edit_design "$id" || true ;;137 8) archive_done "$id" || check_task "$id" || true ;;138 9) [ $((RANDOM % 3)) -eq 0 ] && git -C "$target" add -A && git -C "$target" -c user.name=sim -c user.email=sim@example.com commit -qm "agent-$id checkpoint" && say "$id" "committed checkpoint" || true ;;139 esac140 done141}142143trap 'trap - INT TERM EXIT; kill 0 2>/dev/null' INT TERM EXIT144for i in $(seq 1 "$agents"); do agent "$i" & done145wait