diff --git a/contrib/fx/fx.sh b/contrib/fx/fx.sh index 194b609..be805a7 100755 --- a/contrib/fx/fx.sh +++ b/contrib/fx/fx.sh @@ -26,6 +26,24 @@ # FX_NO_PROBE if set, skip fx's own diagnostic run of the command # (cheaper, but the 30-selfdiag.sh source gets no # output to read and will have no opinion) +# FX_TRACE_FILE where chosen fixes' source labels are recorded, one +# "cmd=source-file" line per accepted fix. Defaults to +# ./p.trace, alongside f()'s own ./p. Written whenever +# a non-blank candidate is handed to f() -- same +# "wrote intent, not confirmed success" semantics as +# p itself (see f.sh). This exists because f() alone +# has no notion of *where* a fix came from: once a +# fix is in p, a manually-typed fix, a locally-known +# fix (10-known.sh), and an LLM-guessed fix +# (70/80-*.sh) are byte-for-byte indistinguishable. +# 80-remote-api.sh's own docstring is explicit that +# LLM-sourced fixes must stay visually distinguishable +# from verified ones -- without this file, that +# distinction is silently lost the moment a fix is +# accepted, and permanently lost if p is disk-persisted +# across a reboot (see adaptive-loop.sh). p.trace is +# what a caller checks before trusting an auto-applied +# fix it didn't personally review. # # Source contract (see sources.d/*.sh): # - any executable file in $FX_SOURCES_DIR @@ -44,6 +62,7 @@ # active later when fx() is actually called, which is almost never the # repo root in practice. FX_SOURCES_DIR="${FX_SOURCES_DIR:-$(pwd)/contrib/fx/sources.d}" +FX_TRACE_FILE="${FX_TRACE_FILE:-p.trace}" fx() { if [ ! -d "$FX_SOURCES_DIR" ]; then @@ -92,12 +111,17 @@ fx() { ' ) - chosen="" + # chosen_line keeps the "[source-label] fix text" form intact so + # provenance can be recovered after picking -- stripping the label + # too early (as this used to do in each branch below) throws away + # the one piece of information that lets a later reader tell an + # LLM-guessed fix apart from a locally-verified one. + chosen_line="" if [ -n "$candidates" ]; then if [ -n "$FX_AUTO" ]; then - chosen=$(printf '%s\n' "$candidates" | head -1 | sed 's/^\[[^]]*\] //') + chosen_line=$(printf '%s\n' "$candidates" | head -1) elif command -v fzf >/dev/null 2>&1; then - chosen=$(printf '%s\n' "$candidates" | fzf --prompt="fix for: $cmd > " --height=40% | sed 's/^\[[^]]*\] //') + chosen_line=$(printf '%s\n' "$candidates" | fzf --prompt="fix for: $cmd > " --height=40%) else i=0 printf '%s\n' "$candidates" | while IFS= read -r line; do @@ -110,9 +134,19 @@ fx() { # f(), same as when zero candidates were found at all. printf 'pick a number (blank to give up): ' read -r n - [ -n "$n" ] && chosen=$(printf '%s\n' "$candidates" | sed -n "${n}p" | sed 's/^\[[^]]*\] //') + [ -n "$n" ] && chosen_line=$(printf '%s\n' "$candidates" | sed -n "${n}p") fi fi + chosen=$(printf '%s\n' "$chosen_line" | sed 's/^\[[^]]*\] //') + chosen_source=$(printf '%s\n' "$chosen_line" | sed -n 's/^\[\([^]]*\)\].*/\1/p') + + # Same "record intent, not confirmed success" timing as f()'s own + # write to p: logged as soon as a non-blank fix is handed off, not + # gated on the retry inside f() actually succeeding. + if [ -n "$chosen" ] && [ -n "$chosen_source" ]; then + printf '%s=%s\n' "$cmd" "$chosen_source" >> "$FX_TRACE_FILE" + fi + printf '%s\n' "$chosen" | f "$@" }