#!/usr/bin/env bash # sx-fix-writeback.sh — close the loop: record new real-git commits from a # fix branch back into the rose-ash/sx-review forge as agentic-sx commits. # # The forge already DRIVES the tmux session (briefing -> launch, sx-fix-up.sh). # This records what comes BACK, so the forge is a true system of record: each # commit on loops/sx- becomes an agentic-sx commit on agents/, # with a small commit.sx pointer (sha/message/files) — real git holds the code. # # Usage: ./scripts/sx-fix-writeback.sh [kind] [base-ref] # forge-agent agentic-sx agent, e.g. ws-W14 (branch = loops/sx-) # kind agentic commit kind: test|refactor|finding|decision (default: test) # base-ref commits are .. (default: architecture) # # Idempotent: commits already recorded in writeback.sxsrc (matched by sha) are skipped. # Re-runs forge-build + replay and re-dumps forge.sxdata. set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)"; cd "$ROOT" AGENT="${1:?usage: sx-fix-writeback.sh [kind] [base-ref]}" KIND="${2:-test}" BASE="${3:-architecture}" SLUG="$(echo "$AGENT" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9' '-' | sed 's/-*$//')" BRANCH="loops/sx-${SLUG}" WB="plans/sx-review/writeback.sxsrc" DUMP="plans/sx-review/forge.sxdata" BIN="hosts/ocaml/_build/default/bin/sx_server.exe" git show-ref --verify --quiet "refs/heads/$BRANCH" || { echo "no branch $BRANCH"; exit 1; } [ -f "$WB" ] || { echo "missing $WB"; exit 1; } # sanitize a commit subject for embedding in an SX string literal sanitize() { echo "$1" | tr -d '"\\' | tr '\n' ' ' | cut -c1-120; } added=0 # oldest-first so replay advances the branch head in order while IFS=$'\x1f' read -r sha subj; do [ -n "$sha" ] || continue if grep -q ":sha \"$sha\"" "$WB"; then continue; fi files="$(git diff-tree --no-commit-id --name-only -r "$sha" | tr '\n' ' ' | sed 's/ *$//')" msg="$(sanitize "$subj")" rec=" {:agent \"$AGENT\" :kind \"$KIND\" :sha \"$sha\" :branch \"$BRANCH\" :message \"$msg\" :files \"$files\"}" # insert before the END-RECORDS marker (keeps chronological order) awk -v r="$rec" '/;;; END-RECORDS/ && !done {print r; done=1} {print}' "$WB" > "$WB.tmp" && mv "$WB.tmp" "$WB" echo "recorded $sha $msg" added=$((added+1)) done < <(git log --reverse --format='%h%x1f%s' "$BASE..$BRANCH") if [ "$added" -eq 0 ]; then echo "no new commits on $BRANCH to write back (all recorded)." exit 0 fi echo "rebuilding forge with $added new write-back record(s) + re-dumping $DUMP ..." RAW="$(mktemp)" { echo '(epoch 1)' for f in spec/stdlib.sx lib/r7rs.sx lib/persist/event.sx lib/persist/backend.sx \ lib/persist/log.sx lib/persist/kv.sx lib/artdag/dag.sx \ lib/datalog/tokenizer.sx lib/datalog/parser.sx lib/datalog/unify.sx \ lib/datalog/db.sx lib/datalog/builtins.sx lib/datalog/aggregates.sx \ lib/datalog/strata.sx lib/datalog/eval.sx lib/datalog/api.sx lib/datalog/magic.sx \ lib/git/object.sx lib/git/ref.sx lib/git/dag.sx lib/git/worktree.sx \ lib/git/diff.sx lib/git/merge.sx lib/git/porcelain.sx \ lib/relations/schema.sx lib/relations/engine.sx lib/relations/api.sx \ lib/relations/explain.sx lib/relations/federation.sx lib/relations/tree.sx \ lib/agentic/schema.sx lib/agentic/branch.sx lib/gitea/repo.sx; do echo "(load \"$f\")" done echo '(epoch 2)' echo '(load "plans/sx-review/forge-build.sxsrc")' echo '(load "plans/sx-review/writeback.sxsrc")' # overrides fb-writeback-records echo '(epoch 3)' echo '(eval "(len (fb-do-writeback))")' # replay -> agentic commits echo '(epoch 4)' echo '(eval "(fb-forge-serialize)")' } | timeout 300 "$BIN" > "$RAW" 2>&1 REPLAYED="$(grep -oE '\(ok 3 [0-9]+\)|\(ok-len 3 [0-9]+\)' "$RAW" | head -1)" python3 - "$RAW" > "$DUMP" <<'PY' import sys, re data = open(sys.argv[1],'rb').read() m = re.search(rb'\(ok-len 4 (\d+)\)\n', data) if not m: sys.stderr.write("FAILED: no dump in forge output\n"); sys.exit(1) n = int(m.group(1)); sys.stdout.buffer.write(data[m.end():m.end()+n]) PY echo "replay result: $REPLAYED ; forge.sxdata now $(wc -c < "$DUMP") bytes" rm -f "$RAW" echo "done. review: git diff plans/sx-review/{writeback.sxsrc,forge.sxdata}"