| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211 |
- #!/usr/bin/env bash
- # Scaffold an outer-loop state spine (loop.config.yaml + STATE.md + run-log.md).
- #
- # Usage: loop-init.sh --name NAME [OPTIONS]
- # Input: argv flags only (no stdin).
- # Output: stdout = the created loop.config.yaml path (data). Under --dry-run, the
- # path then the rendered config. Data only.
- # Stderr: the creation panel, reminders, warnings, errors.
- # Exit: 0 created (or dry-run rendered), 2 usage, 3 template/dir not found,
- # 5 precondition (target dir already populated, no --force)
- #
- # Creates <dir>/<name>/ from the bundled templates, substituting name/pattern/tier/
- # cadence/permission_mode. Never clobbers a populated loop dir. Atomic writes.
- # Next step: fill the config, then `loop-audit.sh <dir>/<name>/loop.config.yaml`.
- #
- # Examples:
- # loop-init.sh --name pr-babysitter --pattern pr-babysitter --tier L1
- # loop-init.sh --name dep-sweeper --pattern dependency-sweeper --tier L2 --cadence 1d
- # loop-init.sh --name nightly --cadence "0 3 * * *" --dry-run
- set -uo pipefail
- readonly EX_OK=0 EX_USAGE=2 EX_NOTFOUND=3 EX_PRECOND=5
- # Terminal design system (skills/_lib/term.sh). stdout = the created path (data);
- # the creation panel frames on stderr, so detect color on fd 2. Degrade to plain
- # stderr lines if the shared lib is unreachable.
- __lib="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../_lib" 2>/dev/null && pwd || true)"
- if [ -n "${__lib:-}" ] && [ -f "$__lib/term.sh" ]; then . "$__lib/term.sh"; term_init 2
- else
- term_panel_open() { :; }; term_panel_close() { :; }; term_panel_vert() { :; }
- term_status_row() { shift; printf ' - %s %s\n' "$1" "${2:-}"; }
- term_alert() { shift; printf ' ! %s\n' "$*"; }
- term_color() { shift; printf '%s' "$*"; }; TERM_DOT="|"
- fi
- HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
- ASSETS="$HERE/../assets"
- CFG_TPL="$ASSETS/loop.config.template.yaml"
- STATE_TPL="$ASSETS/STATE.template.md"
- RUN_TPL="$ASSETS/run.template.md"
- # ── defaults ────────────────────────────────────────────────────────────────
- NAME=""
- PATTERN="custom"
- TIER="L1"
- CADENCE="1h"
- DIR=".loops"
- DRY_RUN=0
- FORCE=0
- usage() {
- cat <<'EOF'
- loop-init.sh — scaffold an outer-loop state spine.
- Usage:
- loop-init.sh --name NAME [OPTIONS]
- Options:
- --name NAME loop identifier, kebab-case (required). Names the directory.
- --pattern KEY catalog key (pr-babysitter, ci-sweeper, dependency-sweeper,
- changelog-drafter, post-merge-cleanup, issue-triage,
- daily-triage) or "custom" (default: custom).
- --tier L1|L2|L3 starting autonomy tier (default: L1).
- --cadence STR 10m | 1h | 6h | 1d, or a cron string (default: 1h).
- --dir DIR parent directory for the loop (default: .loops).
- --dry-run print the target path + rendered config; write nothing.
- --force overwrite an already-populated <dir>/<name>/ directory.
- -h, --help show this help and exit 0.
- Exit codes:
- 0 created (or dry-run) 2 usage 3 template/dir not found 5 dir populated
- Examples:
- loop-init.sh --name pr-babysitter --pattern pr-babysitter --tier L1
- loop-init.sh --name dep-sweeper --pattern dependency-sweeper --tier L2 --cadence 1d
- loop-init.sh --name nightly --cadence "0 3 * * *" --dry-run
- EOF
- }
- die_usage() { printf 'error: %s\n' "$1" >&2; echo >&2; usage >&2; exit "$EX_USAGE"; }
- # ── parse args ──────────────────────────────────────────────────────────────
- while [[ $# -gt 0 ]]; do
- case "$1" in
- --name) [[ $# -ge 2 ]] || die_usage "--name needs a value"; NAME="$2"; shift 2 ;;
- --pattern) [[ $# -ge 2 ]] || die_usage "--pattern needs a value"; PATTERN="$2"; shift 2 ;;
- --tier) [[ $# -ge 2 ]] || die_usage "--tier needs a value"; TIER="$2"; shift 2 ;;
- --cadence) [[ $# -ge 2 ]] || die_usage "--cadence needs a value"; CADENCE="$2"; shift 2 ;;
- --dir) [[ $# -ge 2 ]] || die_usage "--dir needs a value"; DIR="$2"; shift 2 ;;
- --dry-run) DRY_RUN=1; shift ;;
- --force) FORCE=1; shift ;;
- -h|--help) usage; exit "$EX_OK" ;;
- -*) die_usage "unknown flag: $1" ;;
- *) die_usage "unexpected positional argument: $1" ;;
- esac
- done
- # ── validate ────────────────────────────────────────────────────────────────
- [[ -n "$NAME" ]] || die_usage "--name is required"
- [[ "$NAME" =~ ^[a-z0-9]+(-[a-z0-9]+)*$ ]] || die_usage "--name must be kebab-case (got '$NAME')"
- [[ "$PATTERN" =~ ^[a-z0-9]+(-[a-z0-9]+)*$ ]] || die_usage "--pattern must be kebab-case (got '$PATTERN')"
- case "$TIER" in L1|L2|L3) ;; *) die_usage "--tier must be L1|L2|L3 (got '$TIER')" ;; esac
- # cadence: Nm/Nh/Nd OR a cron-ish string (digits, spaces, * / , -)
- [[ "$CADENCE" =~ ^[0-9]+[mhd]$ || "$CADENCE" =~ ^[-0-9*/,\ ]+$ ]] \
- || die_usage "--cadence must be like 10m/1h/1d or a cron string (got '$CADENCE')"
- [[ -f "$CFG_TPL" ]] || { printf 'error: config template not found at %s\n' "$CFG_TPL" >&2; exit "$EX_NOTFOUND"; }
- [[ -f "$STATE_TPL" ]] || { printf 'error: STATE template not found at %s\n' "$STATE_TPL" >&2; exit "$EX_NOTFOUND"; }
- [[ -f "$RUN_TPL" ]] || { printf 'error: run template not found at %s\n' "$RUN_TPL" >&2; exit "$EX_NOTFOUND"; }
- # Default permission_mode from tier (the workhorse mapping; see references/risk-tiers.md).
- case "$TIER" in
- L1|L2) PMODE="dontAsk" ;;
- L3) PMODE="bypassPermissions" ;;
- esac
- TARGET_DIR="$DIR/$NAME"
- CFG_OUT="$TARGET_DIR/loop.config.yaml"
- STATE_OUT="$TARGET_DIR/STATE.md"
- LOG_OUT="$TARGET_DIR/run-log.md"
- RUN_OUT="$TARGET_DIR/run.md"
- # Refuse a populated target unless --force.
- if [[ -d "$TARGET_DIR" ]] && [[ -n "$(ls -A "$TARGET_DIR" 2>/dev/null)" ]] && [[ "$FORCE" -ne 1 ]]; then
- printf 'error: loop directory already populated: %s (use --force to overwrite)\n' "$TARGET_DIR" >&2
- exit "$EX_PRECOND"
- fi
- NOW="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
- # ── render config from template ─────────────────────────────────────────────
- # Line-anchored sed substitutions: identity placeholders globally, the three
- # tunable scalar lines by their default value. Kill-switch path carries <loop-name>.
- render_config() {
- sed -E \
- -e "s|<loop-name>|$NAME|g" \
- -e "s|<pattern-key>|$PATTERN|" \
- -e "s|^tier: L1|tier: $TIER|" \
- -e "s|^cadence: 1h|cadence: $CADENCE|" \
- -e "s|^permission_mode: dontAsk|permission_mode: $PMODE|" \
- "$CFG_TPL"
- }
- render_state() {
- sed -E \
- -e "s|<loop-name>|$NAME|g" \
- -e "s|<ISO-8601 Z>|$NOW|" \
- "$STATE_TPL"
- }
- render_log() {
- cat <<EOF
- # $NAME — run log (append-only; one line per run)
- # format: <ISO-Z> run#N action=<reported|proposed|none> <key=val…> outcome=<…> tokens=<N>
- EOF
- }
- render_run() {
- sed -E \
- -e "s|<loop-name>|$NAME|g" \
- -e "s|tier <L1\\|L2\\|L3>|tier $TIER|g" \
- "$RUN_TPL"
- }
- # ── dry-run: print and stop ─────────────────────────────────────────────────
- if [[ "$DRY_RUN" -eq 1 ]]; then
- printf '%s\n' "$CFG_OUT"
- {
- term_panel_open loop "loop ${TERM_DOT} init (dry-run)" "$NAME"
- term_panel_vert
- term_status_row skip "would create $TARGET_DIR/" "tier $TIER ${TERM_DOT} $PATTERN ${TERM_DOT} $CADENCE"
- term_status_row skip " loop.config.yaml" "permission_mode: $PMODE"
- term_status_row skip " STATE.md / run-log.md / run.md" ""
- term_panel_vert
- term_panel_close "nothing written" ""
- } >&2
- render_config
- exit "$EX_OK"
- fi
- # ── atomic writes ───────────────────────────────────────────────────────────
- mkdir -p "$TARGET_DIR" || { printf 'error: could not create %s\n' "$TARGET_DIR" >&2; exit 1; }
- write_atomic() { # write_atomic <dest> <content>
- local dest="$1" content="$2" tmp
- tmp="$dest.tmp.$$"
- printf '%s\n' "$content" > "$tmp" || { printf 'error: failed to write %s\n' "$tmp" >&2; exit 1; }
- mv -f "$tmp" "$dest" || { rm -f "$tmp"; printf 'error: failed to move into place: %s\n' "$dest" >&2; exit 1; }
- }
- write_atomic "$CFG_OUT" "$(render_config)"
- write_atomic "$STATE_OUT" "$(render_state)"
- write_atomic "$LOG_OUT" "$(render_log)"
- write_atomic "$RUN_OUT" "$(render_run)"
- printf '%s\n' "$CFG_OUT"
- {
- term_panel_open loop "loop ${TERM_DOT} init" "$NAME"
- term_panel_vert
- term_status_row ok "created $TARGET_DIR/" "tier $TIER ${TERM_DOT} $PATTERN ${TERM_DOT} $CADENCE"
- term_status_row ok " loop.config.yaml" "permission_mode: $PMODE"
- term_status_row ok " STATE.md / run-log.md / run.md" ""
- if [[ "$TIER" != "L1" ]]; then
- term_alert warning "tier $TIER needs a verify gate, guard, worktree, escalation + land_via — fill them before auditing"
- fi
- term_panel_vert
- term_panel_close "then: fill the config ${TERM_DOT} loop-audit.sh $CFG_OUT" ""
- } >&2
- exit "$EX_OK"
|