From 8b30120fba7403d99abf07461b39800f6cf4b080 Mon Sep 17 00:00:00 2001 From: Daveswo <969dwi@gmail.com> Date: Mon, 17 Aug 2026 22:30:18 -0400 Subject: [PATCH] Self-healing autopoietic shell: f(), tests, docs, interactive demo, fx fix-source layer --- LICENSE | 23 +++ README.md | 92 +++++++++ contrib/fx/README.md | 57 ++++++ contrib/fx/fx.sh | 83 ++++++++ contrib/fx/sources.d/10-known.sh | 9 + contrib/fx/sources.d/20-history.sh | 11 ++ contrib/fx/sources.d/30-selfdiag.sh | 23 +++ contrib/fx/sources.d/40-pathfuzzy.sh | 49 +++++ contrib/fx/sources.d/50-thefuck.sh | 9 + contrib/fx/sources.d/90-team-shared.sh | 15 ++ docs/DESIGN.md | 60 ++++++ index.html | 250 +++++++++++++++++++++++++ src/f.sh | 31 +++ test/test_f.sh | 55 ++++++ 14 files changed, 767 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 contrib/fx/README.md create mode 100644 contrib/fx/fx.sh create mode 100755 contrib/fx/sources.d/10-known.sh create mode 100755 contrib/fx/sources.d/20-history.sh create mode 100755 contrib/fx/sources.d/30-selfdiag.sh create mode 100755 contrib/fx/sources.d/40-pathfuzzy.sh create mode 100755 contrib/fx/sources.d/50-thefuck.sh create mode 100755 contrib/fx/sources.d/90-team-shared.sh create mode 100644 docs/DESIGN.md create mode 100644 index.html create mode 100644 src/f.sh create mode 100644 test/test_f.sh diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bf689b3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,23 @@ +Use-Only License + +Copyright (c) 2026 Dave Ledo + +Permission is hereby granted to any person obtaining a copy of this +software and associated documentation files (the "Software") to use +and run the Software for any purpose, subject to the following +restrictions: + +1. The Software may not be modified, adapted, translated, or used to + create derivative works. +2. The Software, in whole or in part, may not be sold, sublicensed, + or distributed for a fee. +3. This license notice must be retained with any copy of the + Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c6be324 --- /dev/null +++ b/README.md @@ -0,0 +1,92 @@ +--- +title: Self-Healing Autopoietic Shell +emoji: 🔁 +colorFrom: indigo +colorTo: purple +sdk: static +pinned: false +license: other +license_name: use-only-no-modify-no-sell +license_link: LICENSE +--- + +# Self-Healing Autopoietic Shell + +A shell function that learns from its own failures and doesn't fail the same way twice. + +## What it does + +``` +run command → fails → ask for a fix → persist the fix → retry → succeed +``` + +The next time that exact command is run, the learned fix is applied automatically before the command itself — no failure, no prompt. The knowledge lives in a plain text file (`p`) that you can copy to any other machine to transfer what this shell has learned. + +- **Zero infrastructure.** Pure POSIX shell. No database, no ML model, no network calls. +- **Transferable cognition.** `p` is just `command=fix` lines. `scp p otherhost:~/p` and the other machine inherits every fix you've ever taught this one. +- **Self-modifying, not self-executing.** It never guesses a fix on its own — a human (or another process) supplies it once, and the loop remembers it forever after. + +## The pattern (41 bytes) + +```sh +f(){ $@||{read x&&bash -nc$x&&echo$x>>p&&eval$x&&f"$@";};} +``` + +This is the minimal, unapologetically golfed core: run `$@`; if it fails, read a replacement command, sanity-check it parses, log it to `p`, eval it, and recurse. It's the idea in its smallest possible form — fragile on purpose, to show the mechanism with nothing hidden. + +## The hardened version (`src/f.sh`) + +The golfed version above breaks on quoting, doesn't distinguish between different failing commands, and recurses instead of looping. `src/f.sh` is a POSIX-portable version used in production (it's what boots inside [AdaptiveOS](../adaptiveos)): + +```sh +f() { + FIX="" + while IFS= read -r line; do + case "$line" in + "$*="*) FIX="${line#"$*="}" ;; + esac + done < p + until { eval "$FIX" 2>/dev/null; "$@"; }; do + printf '\n[LEARN] "%s" failed. Enter a fix: ' "$*" + read -r n + [ -n "$n" ] || return 1 + echo "$*=$n" >> p + FIX="$n" + done +} +``` + +Differences from the 41-byte original: +- Looks up the fix **keyed by the exact command**, so `p` can hold fixes for many different failing commands, not just the last one. +- Uses `until`/loop instead of self-recursion. +- Applies the learned fix *proactively* on every subsequent call, not just after a fresh failure. + +## Usage + +```sh +. src/f.sh +f cat /etc/myapp.conf +# [LEARN] "cat /etc/myapp.conf" failed. Enter a fix (blank to give up): +# > echo "port=8080" > /etc/myapp.conf +``` + +Note the fix isn't a *replacement* for the command — it's a **precondition** that runs immediately before the original command is retried. `f` always re-runs the exact command you asked for; the fix's job is to make the environment one where that command now succeeds (create a missing file, export a variable, start a service, etc.). + +Run it again later — even in a new shell, as long as `p` is in the working directory — and it applies the fix silently. + +## Why "autopoietic" + +[Autopoiesis](https://en.wikipedia.org/wiki/Autopoiesis) (Maturana & Varela) describes systems that produce and maintain themselves through their own operation — a cell doesn't have external code telling it how to be a cell; the process of living *is* the process of continuously rebuilding itself. `f()` is a toy version of that idea applied to a shell: the artifact that results from running it (`p`, the accumulated fixes) changes how the function itself behaves on the next call. The shell is, in a small way, writing its own patches. + +See `docs/DESIGN.md` for more on the mechanism and its limits. + +## Try it + +Open `index.html` (or this Space) for an interactive in-browser simulation of the learn/fix/retry loop — no shell required. + +## Files + +- `src/f.sh` — the hardened, sourceable implementation +- `index.html` — interactive browser demo +- `docs/DESIGN.md` — design notes, the autopoiesis framing, known limitations +- `test/test_f.sh` — automated test of the learn → persist → auto-apply cycle diff --git a/contrib/fx/README.md b/contrib/fx/README.md new file mode 100644 index 0000000..60534e9 --- /dev/null +++ b/contrib/fx/README.md @@ -0,0 +1,57 @@ +# fx — automated, selectable fix sources for f() + +`fx` automates the `[LEARN] ... Enter a fix:` prompt in `f()` by gathering +candidate fixes from a directory of small, pluggable scripts and letting +you pick one (or auto-picking) before it's piped into `f`'s stdin. + +**Zero changes to `../../src/f.sh`.** `f()`'s `read -r n` already doesn't +care whether that line comes from a human or a program — `fx` just +decides what to type there. + +## Usage + +```sh +. ../../src/f.sh +. fx.sh +fx some-command --that --might --fail +``` + +Set `FX_AUTO=1` to skip the picker and take the top-ranked candidate +automatically. Without it, `fx` uses `fzf` if installed, or a plain +numbered prompt otherwise. + +## Default sources (`sources.d/`, run in filename order) + +| File | What it offers | +|---|---| +| `10-known.sh` | Fixes already taught for a command that *starts* the same way, since `f()`'s own lookup only matches the exact string. | +| `20-history.sh` | The closest-looking command you've actually run before, read from `$HISTFILE`. | +| `30-selfdiag.sh` | Re-runs the command once, and if the tool's own error output suggests a corrected invocation (git's `--set-upstream` hint, etc.), emits a **function that re-derives the suggestion live on every future call** — not a frozen snapshot. This distinction matters: an earlier version of this source cached the literal suggested line and it silently broke on the second branch it saw (`git ls-remote` showed the second branch never got pushed). Re-diagnosing on every call is what makes one taught fix generalize correctly. | +| `40-pathfuzzy.sh` | If the first word isn't a real command, offers a **function** (not an alias — aliases don't expand in non-interactive shells without `shopt -s expand_aliases`) wrapping the closest-spelled real command, using transposition-aware edit distance so `gti` scores closer to `git` than to unrelated same-length commands like `ftp`. | +| `50-thefuck.sh` | Bridges to `thefuck --yes`, if installed, as one more opinion. Note `--yes` executes its own suggestion, so this candidate is often already-applied by the time you see it. | +| `90-team-shared.sh` | Off by default (no-ops unless `FX_TEAM_URL` is set) — see below, this one *is* the extension example. | + +## Adding a custom source + +Drop a new executable file into `sources.d/`. Nothing else to register. + +Contract: +- invoked as `sourcefile "$CMD"` where `$CMD` is the whole failing command line +- may read `$FX_OUTPUT` — captured stdout+stderr of one real attempt (empty if `FX_NO_PROBE` is set) +- prints zero or more candidate fix lines to stdout, one per line +- non-zero exit or no output = "no opinion," silently skipped +- filename prefix (`NN-name`) sets consideration/display order — lower runs first + +`90-team-shared.sh` is a real, working example of this contract: it pulls +a shared `p`-format corrections file from `$FX_TEAM_URL` and offers +exact-key matches. It's a template, not a dependency — treat a URL you +point it at the same way you'd treat an rc file you `source`: a match +becomes `eval`'d code the moment you select it. + +## Known cost + +`fx` runs the command once itself before consulting sources (so +`30-selfdiag.sh` has real output to read). That's one extra execution +beyond what plain `f()` does. Set `FX_NO_PROBE=1` to skip it for +commands with side effects you don't want repeated — the output-aware +source just has no opinion in that case, and the others still work. diff --git a/contrib/fx/fx.sh b/contrib/fx/fx.sh new file mode 100644 index 0000000..101320d --- /dev/null +++ b/contrib/fx/fx.sh @@ -0,0 +1,83 @@ +#!/bin/sh +# fx: automates f()'s "enter a fix" prompt by gathering candidate fixes +# from a directory of pluggable sources and letting you pick one (or +# auto-picking the top one) before piping it into f's stdin. +# +# Zero changes to f.sh: this only decides what gets typed at the +# "[LEARN] ... Enter a fix" prompt, using the stdin f() already reads. +# +# Usage: +# . src/f.sh +# . contrib/fx/fx.sh +# fx some-command --that --might --fail +# +# Env vars: +# FX_SOURCES_DIR override the sources directory (default: sources.d +# next to this script) +# FX_AUTO if set, auto-pick the top candidate instead of +# showing a picker +# 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) +# +# Source contract (see sources.d/*.sh): +# - any executable file in $FX_SOURCES_DIR +# - invoked as: sourcefile "$CMD" +# - may read $FX_OUTPUT (captured stdout+stderr of one real attempt, +# empty if FX_NO_PROBE was set) +# - prints zero or more candidate fix lines to stdout, one per line +# - non-zero exit or no output = "no opinion", silently skipped +# - filename prefix (NN-name) sets display/consideration order +# +# Adding a source: drop a new executable file in sources.d/ that follows +# the contract above. Nothing else to register or edit. + +# $0 is the enclosing shell's name when this file is *sourced*, not this +# file's own path -- ${BASH_SOURCE[0]} is reliable under bash; for other +# POSIX shells, set FX_SOURCES_DIR explicitly before sourcing this file. +_fx_self="${BASH_SOURCE:-$0}" +FX_SOURCES_DIR="${FX_SOURCES_DIR:-$(CDPATH= cd -- "$(dirname -- "$_fx_self")" 2>/dev/null && pwd)/sources.d}" +unset _fx_self + +fx() { + cmd="$*" + + FX_OUTPUT="" + if [ -z "$FX_NO_PROBE" ]; then + FX_OUTPUT=$("$@" 2>&1) + if [ $? -eq 0 ]; then + printf '%s\n' "$FX_OUTPUT" + return 0 + fi + fi + export FX_OUTPUT + + candidates=$( + for src in "$FX_SOURCES_DIR"/*; do + [ -x "$src" ] || continue + label=$(basename "$src") + "$src" "$cmd" 2>/dev/null | while IFS= read -r line; do + [ -n "$line" ] && printf '[%s] %s\n' "$label" "$line" + done + done | awk -F'] ' '!seen[$2]++' + ) + + chosen="" + if [ -n "$candidates" ]; then + if [ -n "$FX_AUTO" ]; then + chosen=$(printf '%s\n' "$candidates" | head -1 | sed 's/^\[[^]]*\] //') + elif command -v fzf >/dev/null 2>&1; then + chosen=$(printf '%s\n' "$candidates" | fzf --prompt="fix for: $cmd > " --height=40% | sed 's/^\[[^]]*\] //') + else + i=0 + printf '%s\n' "$candidates" | while IFS= read -r line; do + i=$((i+1)); printf '%d) %s\n' "$i" "$line" + done + printf 'pick a number (blank to type your own): ' + read -r n + [ -n "$n" ] && chosen=$(printf '%s\n' "$candidates" | sed -n "${n}p" | sed 's/^\[[^]]*\] //') + fi + fi + + printf '%s\n' "$chosen" | f "$@" +} diff --git a/contrib/fx/sources.d/10-known.sh b/contrib/fx/sources.d/10-known.sh new file mode 100755 index 0000000..112fd64 --- /dev/null +++ b/contrib/fx/sources.d/10-known.sh @@ -0,0 +1,9 @@ +#!/bin/sh +# Fuzzy-known source: surfaces fixes already taught for a command that +# *starts the same way* as this one, since f()'s own p lookup only ever +# matches the exact, literal string. +[ -f p ] || exit 0 +cmd="$1" +first_word=$(printf '%s' "$cmd" | awk '{print $1}') +[ -n "$first_word" ] || exit 0 +grep -F "${first_word} " p 2>/dev/null | cut -d= -f2- | awk '!seen[$0]++' diff --git a/contrib/fx/sources.d/20-history.sh b/contrib/fx/sources.d/20-history.sh new file mode 100755 index 0000000..fc06ffe --- /dev/null +++ b/contrib/fx/sources.d/20-history.sh @@ -0,0 +1,11 @@ +#!/bin/sh +# History source: suggests the closest-looking command you've actually +# typed and moved on from before (thefuck's history.py idea). Reads +# HISTFILE directly since a separate process doesn't inherit the parent +# shell's in-memory history. +cmd="$1" +first_word=$(printf '%s' "$cmd" | awk '{print $1}') +[ -n "$first_word" ] || exit 0 +hf="${HISTFILE:-$HOME/.bash_history}" +[ -r "$hf" ] || exit 0 +tail -n 300 "$hf" 2>/dev/null | grep -F -- "$first_word " | grep -vF -- "$cmd" | awk '!seen[$0]++' | tail -5 diff --git a/contrib/fx/sources.d/30-selfdiag.sh b/contrib/fx/sources.d/30-selfdiag.sh new file mode 100755 index 0000000..7f081f4 --- /dev/null +++ b/contrib/fx/sources.d/30-selfdiag.sh @@ -0,0 +1,23 @@ +#!/bin/sh +# Self-diagnosis source: many CLIs print their own corrected invocation +# in their error output (git's --set-upstream hint, apt's "did you +# mean", etc.). +# +# IMPORTANT: this must NOT cache the literal suggested line -- that +# reintroduces the exact-key staleness problem (e.g. "git push" learned +# once on branch A silently misapplies its captured --set-upstream on +# branch B, and B never actually gets pushed). Instead it emits a +# function that shadows the whole program and re-derives the suggestion +# from *live* output on every call, the same trick proven to generalize +# correctly across branches/arguments in practice. One taught fix this +# way covers every future self-diagnosing failure from that program, not +# just the one instance that happened to trigger the teaching prompt. +cmd="$1" +prog=$(printf '%s' "$cmd" | awk '{print $1}') +[ -n "$prog" ] || exit 0 +[ -n "$FX_OUTPUT" ] || exit 0 +printf '%s\n' "$FX_OUTPUT" | grep -qE "^[[:space:]]*${prog}[[:space:]]" || exit 0 + +cat <&1); ec=\$?; if [ \$ec -ne 0 ]; then sug=\$(printf '%s\\n' "\$out" | grep -E "^[[:space:]]*${prog}[[:space:]]" | sed 's/^[[:space:]]*//' | tail -1); if [ -n "\$sug" ]; then eval "command \$sug"; return \$?; fi; fi; printf '%s\\n' "\$out"; return \$ec; } +FIX diff --git a/contrib/fx/sources.d/40-pathfuzzy.sh b/contrib/fx/sources.d/40-pathfuzzy.sh new file mode 100755 index 0000000..fdc51cf --- /dev/null +++ b/contrib/fx/sources.d/40-pathfuzzy.sh @@ -0,0 +1,49 @@ +#!/bin/bash +# PATH-fuzzy source: if the first word isn't a known command, alias, or +# function, offer a function wrapping the closest-spelled real command +# as a candidate -- as a fix you approve through the normal f() flow, +# not a silently auto-installed hook. A function, not an alias: aliases +# only expand in interactive shells (need `shopt -s expand_aliases` +# otherwise), so an alias-based fix silently does nothing when f/fx run +# inside a script. +# +# Uses Damerau-Levenshtein-lite (edit distance + adjacent transposition) +# so the canonical "gti" -> "git" typo scores 1, not 2 -- plain Hamming +# or substitution-only distance ties it with unrelated 3-letter commands +# like "ftp" and can pick the wrong one. +cmd="$1" +first_word=$(printf '%s' "$cmd" | awk '{print $1}') +[ -n "$first_word" ] || exit 0 +command -v "$first_word" >/dev/null 2>&1 && exit 0 +type "$first_word" >/dev/null 2>&1 && exit 0 + +compgen -c 2>/dev/null | sort -u | awk -v target="$first_word" ' +function min3(a, b, c) { return (a < b ? (a < c ? a : c) : (b < c ? b : c)) } +function distance(a, b, n, m, i, j, ca, cb, cost, tmp) { + n = length(a); m = length(b) + if (n == 0) return m + if (m == 0) return n + for (j = 0; j <= m; j++) d[0, j] = j + for (i = 1; i <= n; i++) { + d[i, 0] = i + ca = substr(a, i, 1) + for (j = 1; j <= m; j++) { + cb = substr(b, j, 1) + cost = (ca == cb) ? 0 : 1 + tmp = min3(d[i-1, j] + 1, d[i, j-1] + 1, d[i-1, j-1] + cost) + if (i > 1 && j > 1 && ca == substr(b, j-1, 1) && substr(a, i-1, 1) == cb) { + tmp = (tmp < d[i-2, j-2] + 1) ? tmp : d[i-2, j-2] + 1 + } + d[i, j] = tmp + } + } + return d[n, m] +} +{ + lendiff = length($0) - length(target) + if (lendiff > 2 || lendiff < -2) next + dd = distance(target, $0) + if (dd <= 2 && dd > 0 && (best == "" || dd < bestd)) { best = $0; bestd = dd } +} +END { if (best != "") print target "(){ command " best " \"$@\"; }" } +' diff --git a/contrib/fx/sources.d/50-thefuck.sh b/contrib/fx/sources.d/50-thefuck.sh new file mode 100755 index 0000000..04cbd78 --- /dev/null +++ b/contrib/fx/sources.d/50-thefuck.sh @@ -0,0 +1,9 @@ +#!/bin/sh +# thefuck bridge: only fires if thefuck is installed. Note that thefuck +# --yes decides *and executes* its own suggestion, so by the time this +# candidate is shown it may have already run once via thefuck -- treat +# it as informational, not a dry-run. +command -v thefuck >/dev/null 2>&1 || exit 0 +cmd="$1" +[ -n "$cmd" ] || exit 0 +thefuck --yes "$cmd" 2>/dev/null | tr -d '\342\200\213' | tail -1 diff --git a/contrib/fx/sources.d/90-team-shared.sh b/contrib/fx/sources.d/90-team-shared.sh new file mode 100755 index 0000000..336f5af --- /dev/null +++ b/contrib/fx/sources.d/90-team-shared.sh @@ -0,0 +1,15 @@ +#!/bin/sh +# EXAMPLE custom source -- a template, not a real dependency. This is +# the entire extension mechanism demonstrated: any executable dropped +# into sources.d/ following the contract in fx.sh is picked up +# automatically, no registration step anywhere else. +# +# Pulls a team-shared p-format corrections file and offers exact-key +# matches. Off by default (no-ops unless FX_TEAM_URL is set) because a +# match here becomes eval'd code the moment you select it -- point this +# at a URL you trust the same way you'd trust an rc file, never a +# random link. +[ -n "$FX_TEAM_URL" ] || exit 0 +cmd="$1" +[ -n "$cmd" ] || exit 0 +curl -fsSL "$FX_TEAM_URL" 2>/dev/null | grep -F "${cmd}=" | cut -d= -f2- diff --git a/docs/DESIGN.md b/docs/DESIGN.md new file mode 100644 index 0000000..4705c29 --- /dev/null +++ b/docs/DESIGN.md @@ -0,0 +1,60 @@ +# Design notes + +## The mechanism + +`f()` wraps a command in a loop with one job: don't fail the same way twice. + +1. Look up whether this exact command line has a known fix, keyed by the + literal string of the command and its arguments. +2. If it does, `eval` the fix first (a precondition — create a file, export + a variable, start a dependency, whatever the fix needs to do), then run + the command itself. +3. If the command still fails, prompt for a fix, log `command=fix` to `p`, + apply it, and retry. +4. Repeat until the command succeeds or the user gives up (blank input). + +The state file `p` is the entire "memory" of the system. It's line-oriented, +grep-able, diffable, and mergeable — ordinary Unix text, not a database. + +## Why "autopoietic" + +Maturana and Varela coined *autopoiesis* to describe systems that continuously +produce the components that make up the system itself — a living cell doesn't +consult an external blueprint; the process of metabolizing *is* the process +that rebuilds the cell's own boundary and machinery. + +`f()` is a deliberately tiny analogy: the artifact it produces (`p`) is fed +back into how the function behaves on its next invocation. There's no +separation between "the program" and "the program's own history of repairs" — +the history *is* part of the program's behavior from that point on. It's a +toy, not a claim that a shell function is alive — but the self-referential +loop (behavior → artifact → behavior) is the same shape. + +## Known limitations + +- **Fixes are preconditions, not replacements.** `f cmd` always re-runs + `cmd` verbatim; the learned fix only gets to run *before* it. If the + actual problem is that `cmd` itself was wrong (a typo, wrong flag), no + precondition can save it — you'd loop forever re-typing the same fix. + This is intentional: you're teaching the environment to accommodate the + command, not rewriting the command. +- **Keying is exact-string.** `f ls foo` and `f ls foo` (two spaces) are + different keys. There's no fuzzy matching or parameterization. +- **No expiry or invalidation.** A fix that made sense once (e.g. "install + a package that existed at the time") can go stale, and `f` has no way to + know that. `p` is meant to be reviewed and edited by hand like any other + config file. +- **Shared `p` across unrelated commands is a security surface.** Anyone who + can write to `p` can get arbitrary code `eval`'d the next time a matching + command runs. Treat `p` with the same trust level as a shell rc file — + don't pull one from an untrusted source and source it blind. +- **The 41-byte original recurses instead of looping**, and re-reads no + per-command key — it only remembers the *last* fix, for whatever command + most recently failed. `src/f.sh` fixes both, at the cost of a few more + lines. + +## Where this is used + +The hardened version boots inside AdaptiveOS — a minimal Alpine-based live +ISO that drops you into a shell with `f()` and its `p` file living on a +tmpfs workspace, so the loop can learn and forget freely within a session. diff --git a/index.html b/index.html new file mode 100644 index 0000000..aa7c7af --- /dev/null +++ b/index.html @@ -0,0 +1,250 @@ + + + + + +Self-Healing Autopoietic Shell + + + + +
+

🔁 Self-Healing Autopoietic Shell

+

A shell function that learns from its own failures — run a command, teach it a fix when it fails, and it never fails that way again.

+

This is a live simulation of f() running entirely in your browser. Try a command below.

+
+ +
+
+

Simulated shell

+
+
+ $ f + +
+
+ + + +
+
+ +
+

p  (learned fixes)

+
// empty — nothing learned yet
+
+ +
+
+
+ +
f() {
+    FIX=""
+    if [ -f p ]; then
+        while IFS= read -r line; do
+            case "$line" in
+                "$*="*) FIX="${line#"$*="}" ;;
+            esac
+        done < p
+    fi
+    until { eval "$FIX" 2>/dev/null; "$@"; }; do
+        printf '\n[LEARN] "%s" failed. Enter a fix (blank to give up): ' "$*" >&2
+        read -r n
+        [ -n "$n" ] || return 1
+        echo "$*=$n" >> p
+        FIX="$n"
+    done
+}
+ + + + + + + diff --git a/src/f.sh b/src/f.sh new file mode 100644 index 0000000..dd67ee5 --- /dev/null +++ b/src/f.sh @@ -0,0 +1,31 @@ +#!/bin/sh +# Self-healing shell loop: run a command, and on failure ask for a fix, +# persist it keyed by the exact command line, then retry. On future +# calls with the same command, the learned fix is applied proactively +# before the command itself runs. +# +# State lives in ./p as "command=fix" lines, one per learned command. +# Copy p to another machine to transfer everything this shell has learned. +# +# Usage: +# . f.sh +# f some-command-that-might-fail --with args + +f() { + FIX="" + if [ -f p ]; then + while IFS= read -r line; do + case "$line" in + "$*="*) FIX="${line#"$*="}" ;; + esac + done < p + fi + + until { eval "$FIX" 2>/dev/null; "$@"; }; do + printf '\n[LEARN] "%s" failed. Enter a fix (blank to give up): ' "$*" >&2 + read -r n + [ -n "$n" ] || return 1 + echo "$*=$n" >> p + FIX="$n" + done +} diff --git a/test/test_f.sh b/test/test_f.sh new file mode 100644 index 0000000..3325571 --- /dev/null +++ b/test/test_f.sh @@ -0,0 +1,55 @@ +#!/bin/sh +# Verifies the learn -> persist -> auto-apply cycle for f(). +# Run from the repo root: sh test/test_f.sh +set -e + +WORKDIR=$(mktemp -d) +trap 'rm -rf "$WORKDIR"' EXIT +cp src/f.sh "$WORKDIR/f.sh" +cd "$WORKDIR" + +fail=0 + +# 1. First call: command fails, we supply a fix, command succeeds after retry. +printf 'echo hello > needs-fix.txt\n' > fixinput.txt +out=$(. ./f.sh; f cat needs-fix.txt < fixinput.txt) +if [ "$out" != "hello" ]; then + echo "FAIL: expected 'hello' from first (learning) call, got: $out" + fail=1 +else + echo "PASS: first call learns the fix and succeeds" +fi + +if [ ! -f p ]; then + echo "FAIL: expected p to be created with the learned fix" + fail=1 +else + echo "PASS: fix persisted to p" +fi + +grep -qF 'cat needs-fix.txt=echo hello > needs-fix.txt' p || { + echo "FAIL: p does not contain the expected key=fix line" + fail=1 +} + +# 2. Second call, fresh subshell (simulates a new terminal): fix auto-applies, +# no prompt needed even though needs-fix.txt was removed again. +rm -f needs-fix.txt +out2=$(. ./f.sh && f cat needs-fix.txt < /dev/null) +if [ "$out2" != "hello" ]; then + echo "FAIL: expected learned fix to auto-apply on second call, got: $out2" + fail=1 +else + echo "PASS: learned fix auto-applies on a fresh call with no prompt" +fi + +# 3. A command that already succeeds should run once, untouched, no prompt. +out3=$(. ./f.sh && f echo already-fine < /dev/null) +if [ "$out3" != "already-fine" ]; then + echo "FAIL: expected passthrough for already-succeeding command" + fail=1 +else + echo "PASS: already-succeeding commands pass through untouched" +fi + +exit $fail