/ devlog / devscripts / settle_gold.sh
#!/bin/sh
# Settles gold, or reports it unchanged, and prints its head.
#
# Run this as the first action of every round, always, with the commit
# message Chris showed for the last delivered patch, or with no argument
# when he showed none. Paste what it prints; never write gold's head
# from memory.
#
#   settle_gold.sh "a page that fronts the news"   settles the oldest
#                                                  delivered patch
#   settle_gold.sh                                 settles nothing
#
# GOLD and LEDGER may be set in the environment; the defaults suit a
# sandbox with gold at /home/claude/gold.
GOLD=${GOLD:-/home/claude/gold}
WORK=${WORK:-/home/claude/work}
LEDGER=${LEDGER:-/home/claude/gold_ledger.txt}
# A delivery Chris could not apply is withdrawn rather than settled: gold
# never receives it, its line leaves the ledger, and its file is deleted,
# so the next cut is not blocked by a patch that no longer exists.
if [ "$1" = "--withdraw" ]; then
    if [ -z "$2" ]; then
        echo "settle: --withdraw needs the name of the patch"
        exit 1
    fi
    if ! grep -q " $2 delivered\$" "$LEDGER"; then
        echo "settle: no delivered patch named $2"
        exit 1
    fi
    grep -v " $2 delivered\$" "$LEDGER" > "$LEDGER.tmp"
    mv "$LEDGER.tmp" "$LEDGER"
    rm -f "$OUT/$2.patch"
    cksum < "$LEDGER" | awk '{print $1"-"$2}' > "$LEDGER.sum"
    echo "settle: withdrew $2; gold left alone"
    exit 0
fi
# A settle that should never have happened is undone here. Gold only
# ever advances on a commit message Chris showed; where a patch was
# applied without one, gold holds work his tree does not, and every
# later patch is cut against a base he cannot apply. Undoing it moves
# gold back one commit and puts the patch back to waiting, so it can be
# delivered again. The commit being undone has to be gold's head, so
# only the most recent settle can be taken back.
#
#   settle_gold.sh --unsettle <name-of-patch>
#
if [ "$1" = "--unsettle" ]; then
    if [ -z "$2" ]; then
        echo "settle: --unsettle needs the name of the patch"
        exit 1
    fi
    if ! grep -q " $2 settled\$" "$LEDGER"; then
        echo "settle: no settled patch named $2"
        exit 1
    fi
    last=`grep ' settled$' "$LEDGER" | tail -1 | awk '{print $2}'`
    if [ "$last" != "$2" ]; then
        echo "settle: $2 is not the last patch settled; only the last"
        echo "  one can be taken back."
        exit 1
    fi
    git -C "$GOLD" reset --hard HEAD^ > /dev/null 2>&1
    git -C "$GOLD" tag -f confirmed-head HEAD > /dev/null 2>&1
    sed "s|^\(.* $2\) settled\$|\1 delivered|" "$LEDGER" > \
        "$LEDGER.tmp"
    mv "$LEDGER.tmp" "$LEDGER"
    cksum < "$LEDGER" | awk '{print $1"-"$2}' > "$LEDGER.sum"
    echo "settle: took back $2; gold is now at"
    echo "  `git -C \"$GOLD\" log -1 --format='%h %s'`"
    exit 0
fi
# A patch Chris wrote himself is put into gold the same way, by this
# script and not by hand, so the ledger records it and gold's head keeps
# carrying the message of what was last settled.
#
#   settle_gold.sh --his <file> "the message it is committed under"
#
# The count resets when it has reached ten and the review has been
# written, and at no other time. An early re-read, which Chris asks for
# when he sees drift, leaves the count where it is: the ten-round cycle
# is his measure of how long a session has run, not a thing to be
# cleared by reading out of turn. Both halves are checked here rather
# than left to memory: the count has to be ten or more, and the review
# has to name most of the file's own sections.
if [ "$1" = "--read" ]; then
    rounds=${ROUNDS:-/home/claude/round_count.txt}
    said=${SAID:-/home/claude/said_this_round.txt}
    review=${2:-}
    count=`awk '{print $1}' "$rounds" 2>/dev/null`
    if [ -z "$count" ]; then
        count=0
    fi
    if [ "$count" -lt 10 ]; then
        echo "settle: the count is $count, so this is an early re-read"
        echo "  and the count stays where it is. Only a re-read at ten"
        echo "  or more resets it."
        exit 1
    fi
    if [ -z "$review" ] || [ ! -f "$review" ]; then
        echo "settle: name the file holding the review. The count"
        echo "  resets when the review has been written, not before."
        exit 1
    fi
    named=0
    wanted=0
    for section in "gold and work" "dev environment" "Presenting Options" \
        "Coding conventions" "docblock" "Yioop-specific rules" "Tests" \
        "audit" "Verify before you state" "Delivering a patch" "Devlog" \
        "Spelling" "Words and phrases" "Acronyms" \
        "Running things" "Functions that do nothing" "Deprecations"
    do
        wanted=`expr $wanted + 1`
        if grep -q -i -- "$section" "$review"; then
            named=`expr $named + 1`
        fi
    done
    if [ "$named" -lt `expr $wanted - 2` ]; then
        echo "settle: the review names $named of $wanted sections, which"
        echo "  is too few. Write a line or two on each before the count"
        echo "  resets."
        exit 1
    fi
    # A review already used cannot reset the count again: the reset
    # stands for a review written this round, and the same file
    # offered twice is not that.
    seen=${REVIEWS_SEEN:-/home/claude/reviews_used.txt}
    mark=`cksum < "$review" | awk '{print $1"-"$2}'`
    if [ -f "$seen" ] && grep -q "^$mark\$" "$seen"; then
        echo "settle: refused; that review was used before. The"
        echo "  count resets on a review written this round, not"
        echo "  on a file offered again."
        exit 1
    fi
    echo "$mark" >> "$seen"
    words=`cksum < "$said" 2>/dev/null | awk '{print $1"-"$2}'`
    if [ -z "$words" ]; then
        words="none"
    fi
    # A recorded review does not clear the count. The response has to
    # carry it, which check_words.sh sees when it reads the draft, so
    # the review is set aside here for that check to find.
    owed=${REVIEW_OWED:-/home/claude/review_owed.txt}
    head -40 "$review" > "$owed"
    echo "settle: the review is recorded and owed to Chris. It named"
    echo "  $named of $wanted sections. The count clears when the"
    echo "  response carries it, which check_words.sh checks."
    exit 0
fi
# A settle that put the wrong thing into gold is undone here rather than
# by hand: gold goes back one commit and the patch waits again.
if [ "$1" = "--undo" ]; then
    cd "$GOLD" || exit 1
    message=`git log -1 --format=%s`
    git reset -q --hard HEAD~1
    git tag -f confirmed-head HEAD > /dev/null
    name=`echo "$message" | tr 'A-Z' 'a-z' | tr -c 'a-z0-9' '-' | \
        sed 's/--*/-/g; s/^-//; s/-$//'`
    sed "s|\(.*$name\) settled\$|\1 delivered|" "$LEDGER" > "$LEDGER.tmp"
    mv "$LEDGER.tmp" "$LEDGER"
    cksum < "$LEDGER" | awk '{print $1"-"$2}' > "$LEDGER.sum"
    echo "settle: undid \"$message\"; it waits again"
    echo "gold: `git log --oneline -1`"
    exit 0
fi
if [ "$1" = "--his" ]; then
    if [ -z "$2" ] || [ -z "$3" ]; then
        echo "settle: --his needs a patch file and a message"
        exit 1
    fi
    cd "$GOLD" || exit 1
    if ! git apply "$2"; then
        echo "settle: gold would not take $2; gold left alone"
        exit 1
    fi
    git add -A
    git -c user.email=llm@dev.null -c user.name=LLM commit -q -m "$3"
    git tag -f confirmed-head HEAD > /dev/null
    if [ -d "$WORK/.git" ]; then
        (cd "$WORK" && git tag -f confirmed-head HEAD > /dev/null)
    fi
    name=`echo "$3" | tr 'A-Z' 'a-z' | tr -c 'a-z0-9' '-' | \
        sed 's/--*/-/g; s/^-//; s/-$//'`
    echo "$2 $name settled" >> "$LEDGER"
    cksum < "$LEDGER" | awk '{print $1"-"$2}' > "$LEDGER.sum"
    echo "settle: gold took Chris's own patch"
    echo "gold: `git log --oneline -1`"
    exit 0
fi
# The round counter lives in a file rather than in a response, so it
# cannot be forgotten. Every settle counts one round and says when the
# process file is due to be read again; the cut refuses while it is due.
# A round is one thing Chris said, not one run of this script, so the
# count follows his words rather than the running: what he said is
# written to SAID before anything else, and the count moves only when
# those words differ from the ones it last counted. Running the settle
# twice in a round, or withdrawing and settling, counts once. The count
# and the checksum of his words are kept together in the file, so
# neither the number nor which round it belongs to can drift.
ROUNDS=${ROUNDS:-/home/claude/round_count.txt}
SAID=${SAID:-/home/claude/said_this_round.txt}
if [ ! -f "$ROUNDS" ]; then
    echo "0 none" > "$ROUNDS"
fi
count=`awk '{print $1}' "$ROUNDS"`
counted_words=`awk '{print $2}' "$ROUNDS"`
said_words=`cksum < "$SAID" 2>/dev/null | awk '{print $1"-"$2}'`
if [ -z "$said_words" ]; then
    said_words="none"
fi
if [ "$said_words" != "$counted_words" ]; then
    count=`expr $count + 1`
    echo "$count $said_words" > "$ROUNDS"
    # What each counted round was about is written down beside the
    # number. Without this the count is a bare number, and when it does
    # not match what a response claimed there is no way to see which
    # rounds it counted.
    echo "$count `head -c 60 "$SAID" | tr '\n' ' '`" >> "$ROUNDS.log"
fi
echo "round count: $count"
# At ten the re-read is not a reminder, it is the round, so the count
# stops the round's work below. Settling comes first all the same:
# stopping before it left Chris's commit unapplied, so the next round
# cut its patch against a gold a commit behind his tree, and a change he
# had already taken came back to him a second time.
DUE=0
if [ "$count" -ge 10 ]; then
    DUE=1
fi
touch "$LEDGER"
# The only thing that advances gold is Chris showing the commit message
# for the delivered patch. To settle, his words have to be on hand: his
# message of the round is kept in SAID, written there before anything
# else is run, and the message given here has to appear in it. A message
# that is not in his round's words settles nothing, so gold cannot move
# on words that were never said.
SAID=${SAID:-/home/claude/said_this_round.txt}
if [ -n "$1" ] && [ "$1" != "--withdraw" ]; then
    if [ ! -s "$SAID" ]; then
        echo "settle: $SAID is empty, so there is nothing Chris said to"
        echo "  check the message against; gold left alone."
        exit 1
    fi
    if ! grep -qF -- "$1" "$SAID"; then
        echo "settle: \"$1\" is not in what Chris said this round;"
        echo "  gold left alone. Settle only on his own words."
        exit 1
    fi
fi
if [ -n "$1" ]; then
    line=`grep ' delivered$' "$LEDGER" | head -1`
    if [ -z "$line" ]; then
        echo "settle: nothing is waiting; gold left alone"
    else
        patch=`echo "$line" | cut -d' ' -f1`
        # The shown message has to name the waiting patch, not merely be
        # something Chris said. A patch is named for what it does, and
        # the ledger holds that name, so the message he shows for it
        # carries the same words. Where the message names a patch already
        # settled, or another patch, settling the waiting one under it
        # puts the waiting tree into gold under the wrong message: that
        # is the drift check_sync then reports and cut_patch refuses on.
        # The message and the name are compared with runs of non-letters
        # flattened, so "the save as mark" matches the name
        # "the-save-as-mark".
        waiting_name=`echo "$line" | cut -d' ' -f2`
        message_flat=`echo "$1" | tr 'A-Z' 'a-z' | tr -c 'a-z0-9' '-' | \
            sed 's/--*/-/g; s/^-//; s/-$//'`
        name_flat=`echo "$waiting_name" | tr 'A-Z' 'a-z'`
        if ! echo "$message_flat" | grep -qF -- "$name_flat"; then
            echo "settle: the message shown is \"$1\", which does not name"
            echo "  the waiting patch \"$waiting_name\"; gold left alone."
            echo "  Show the message that goes with the waiting patch."
            echo "round count: $count"
            echo "gold: `cd "$GOLD" && git log --oneline -1`"
            exit 1
        fi
        # Gold takes what work holds, rather than the delivered file.
        # The two are the same whenever the delivery was cut from gold,
        # but where a delivery had to be cut against a rebuilt tree, so
        # that Chris could apply it, only work's own diff will go on to
        # gold. Settling from the file in that case leaves gold unable
        # to take its own patch.
        # What Chris applied is the delivered file, so that file is what
        # gold takes. Work's own diff is a fallback for a delivery that
        # had to be cut against a rebuilt tree, and it is wrong whenever
        # the delivery carried only some of what work holds.
        changes="$GOLD/../settle_changes.patch"
        if [ -s "$patch" ]; then
            cp "$patch" "$changes"
        elif [ -d "$WORK/.git" ]; then
            (cd "$WORK" && git diff confirmed-head..HEAD) > "$changes"
        fi
        # Work is cloned afresh after every settle, so its diff is empty
        # whenever the round has already re-cloned it. The delivered file
        # is then what gold takes.
        if [ ! -s "$changes" ]; then
            cp "$patch" "$changes"
        fi
        cd "$GOLD" || exit 1
        if git apply "$changes"; then
            git add -A
            git -c user.email=llm@dev.null -c user.name=LLM \
                commit -q -m "$1"
            git tag -f confirmed-head HEAD > /dev/null
            # work's marker has to move with gold's, or the next cut
            # carries hunks gold already holds and will not apply
            if [ -d "$WORK/.git" ]; then
                (cd "$WORK" && git tag -f confirmed-head HEAD > /dev/null)
            fi
            grep -v "^$patch " "$LEDGER" > "$LEDGER.next"
            echo "$patch `echo "$line" | cut -d' ' -f2` settled" \
                >> "$LEDGER.next"
            mv "$LEDGER.next" "$LEDGER"
        else
            echo "settle: $patch would not apply; gold left alone"
        fi
    fi
fi
cd "$GOLD" || exit 1
echo "gold: `git log -1 --format='%h %s'`"
waiting=`grep -c ' delivered$' "$LEDGER"`
if [ "$waiting" -gt 0 ]; then
    echo "waiting to be settled:"
    grep ' delivered$' "$LEDGER"
fi
# Record what the ledger looks like after this script wrote it, so a later
# hand edit can be found.
cksum < "$LEDGER" | awk '{print $1"-"$2}' > "$LEDGER.sum"
if [ "$DUE" = "1" ]; then
    echo ""
    echo "STOP. The count is $count, so this round is the re-read."
    echo "  Gold has been settled, which always happens. Nothing else:"
    echo "  read devlog/dev_process_for_llms.html whole, write the"
    echo "  review, then run:"
    echo "    sh devlog/devscripts/settle_gold.sh --read <review-file>"
    exit 2
fi
X