From e9f51827d6a7111d98a439ea1295e4f9c0d376fc Mon Sep 17 00:00:00 2001 From: Daveswo <969dwi@gmail.com> Date: Sat, 22 Aug 2026 01:25:02 -0400 Subject: [PATCH] fx: preserve fix-source provenance in a separate p.trace file Previously fx() stripped the "[source-label]" prefix before handing a chosen fix to f(), so once a fix landed in p, a manually-typed fix, a locally-known fix, and an LLM-guessed fix (70-local-llm.sh, 80-remote-api.sh) were byte-for-byte indistinguishable. That silently undermines 80-remote-api.sh's own documented trust boundary, which requires LLM-sourced fixes to stay visually distinguishable from verified ones -- and the distinction was permanently lost once p is disk-persisted across a reboot (adaptive-loop.sh). fx() now records "cmd=source-file" to FX_TRACE_FILE (default ./p.trace) alongside every fix it hands to f(), using the same "record intent, not confirmed success" timing f() already uses for p itself. Verified end-to-end against a stub 80-remote-api.sh-style source: p and p.trace both populate correctly, and the retried command actually succeeds. --- contrib/fx/fx.sh | 42 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 4 deletions(-) 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 "$@" }