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.
This commit is contained in:
2026-08-22 01:25:02 -04:00
parent 602e2ee4bd
commit e9f51827d6
+38 -4
View File
@@ -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 "$@"
}