Self-healing autopoietic shell: f(), tests, docs, interactive demo, fx fix-source layer

This commit is contained in:
2026-08-17 22:30:18 -04:00
commit 8b30120fba
14 changed files with 767 additions and 0 deletions
+23
View File
@@ -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.
+92
View File
@@ -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
+57
View File
@@ -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.
+83
View File
@@ -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 "$@"
}
+9
View File
@@ -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]++'
+11
View File
@@ -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
+23
View File
@@ -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 <<FIX
${prog}(){ out=\$(command ${prog} "\$@" 2>&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
+49
View File
@@ -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 " \"$@\"; }" }
'
+9
View File
@@ -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
+15
View File
@@ -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-
+60
View File
@@ -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.
+250
View File
@@ -0,0 +1,250 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Self-Healing Autopoietic Shell</title>
<style>
:root{
--bg:#0b0d10; --panel:#12151a; --ink:#e6e9ef; --dim:#8b93a1;
--accent:#7c9eff; --ok:#5fd88f; --warn:#f2b64b; --err:#ff6b6b;
--border:#232833;
}
*{box-sizing:border-box;}
body{
margin:0; background:var(--bg); color:var(--ink);
font:15px/1.55 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
header{
padding:2.5rem 1.5rem 1.25rem; max-width:900px; margin:0 auto;
}
h1{font-size:1.4rem; margin:0 0 .4rem; font-weight:600;}
header p{color:var(--dim); margin:.2rem 0; font-size:.92rem;}
main{max-width:900px; margin:0 auto; padding:0 1.5rem 3rem; display:grid; gap:1.25rem; grid-template-columns:1fr; }
@media (min-width:760px){ main{grid-template-columns: 1.3fr .9fr;} }
.panel{
background:var(--panel); border:1px solid var(--border); border-radius:10px;
overflow:hidden;
}
.panel h2{
font-size:.78rem; text-transform:uppercase; letter-spacing:.06em; color:var(--dim);
margin:0; padding:.7rem .9rem; border-bottom:1px solid var(--border);
}
#term{
padding:.9rem; height:360px; overflow-y:auto; white-space:pre-wrap; word-break:break-word;
font-size:.88rem;
}
#term .line{margin:0 0 .15rem;}
.prompt{color:var(--accent);}
.learn{color:var(--warn);}
.ok{color:var(--ok);}
.err{color:var(--err);}
.dim{color:var(--dim);}
form{display:flex; border-top:1px solid var(--border);}
form span{padding:.6rem .9rem; color:var(--accent);}
input{
flex:1; background:transparent; border:0; color:var(--ink); font:inherit;
padding:.6rem .9rem .6rem 0; outline:none;
}
#pfile{padding:.9rem; min-height:120px; font-size:.85rem;}
#pfile .row{color:var(--ok); margin:0 0 .3rem;}
#pfile .empty{color:var(--dim);}
.try{padding:.9rem; border-top:1px solid var(--border); display:flex; flex-wrap:wrap; gap:.4rem;}
.try button{
background:#171b22; border:1px solid var(--border); color:var(--ink);
font:inherit; font-size:.78rem; padding:.35rem .6rem; border-radius:6px; cursor:pointer;
}
.try button:hover{border-color:var(--accent); color:var(--accent);}
code{background:#171b22; padding:.1rem .35rem; border-radius:4px; font-size:.85em;}
pre{
background:#171b22; padding:.9rem; border-radius:8px; overflow-x:auto;
font-size:.82rem; border:1px solid var(--border); margin:.8rem 1.5rem 0; max-width:900px;
}
footer{max-width:900px; margin:0 auto; padding:0 1.5rem 3rem; color:var(--dim); font-size:.82rem;}
footer a{color:var(--accent);}
.reset{color:var(--dim); font-size:.78rem; background:none; border:0; cursor:pointer; text-decoration:underline; padding:0;}
</style>
</head>
<body>
<header>
<h1>🔁 Self-Healing Autopoietic Shell</h1>
<p>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.</p>
<p class="dim">This is a live simulation of <code>f()</code> running entirely in your browser. Try a command below.</p>
</header>
<main>
<div class="panel">
<h2>Simulated shell</h2>
<div id="term"></div>
<form id="form">
<span>$ f</span>
<input id="cmdInput" autocomplete="off" placeholder="cat needs-fix.txt" autofocus>
</form>
<div class="try">
<button data-cmd="cat needs-fix.txt">cat needs-fix.txt</button>
<button data-cmd="curl api.example.com">curl api.example.com</button>
<button data-cmd="echo already-fine">echo already-fine</button>
</div>
</div>
<div class="panel">
<h2>p &nbsp;<span class="dim" style="text-transform:none;">(learned fixes)</span></h2>
<div id="pfile"><div class="empty">// empty — nothing learned yet</div></div>
<div class="try">
<button class="reset" id="resetBtn">reset memory</button>
</div>
</div>
</main>
<pre>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
}</pre>
<footer>
Real source, tests, and design notes are in the repo alongside this page —
see <code>src/f.sh</code>, <code>test/test_f.sh</code>, and <code>docs/DESIGN.md</code>.
</footer>
<script>
(function(){
const term = document.getElementById('term');
const form = document.getElementById('form');
const input = document.getElementById('cmdInput');
const pfileEl = document.getElementById('pfile');
const resetBtn = document.getElementById('resetBtn');
// "p" — the learned-fixes store. Simulated filesystem state lives here.
let p = {}; // command -> fix
let fs = {}; // simulated tiny filesystem for the demo commands
let awaitingFix = null; // command string we're currently learning a fix for
function print(text, cls){
const div = document.createElement('div');
div.className = 'line' + (cls ? ' ' + cls : '');
div.textContent = text;
term.appendChild(div);
term.scrollTop = term.scrollHeight;
}
function renderP(){
const keys = Object.keys(p);
if(!keys.length){
pfileEl.innerHTML = '<div class="empty">// empty — nothing learned yet</div>';
return;
}
pfileEl.innerHTML = keys.map(k =>
`<div class="row">${escapeHtml(k)}=${escapeHtml(p[k])}</div>`
).join('');
}
function escapeHtml(s){
return s.replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
}
// Applies a learned fix (or a freshly supplied one) to the tiny simulated fs/state.
function applyFix(fix){
const m = fix.match(/^echo\s+(.*?)\s*>\s*(\S+)$/);
if(m){
let val = m[1].trim();
if((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))){
val = val.slice(1,-1);
}
fs[m[2]] = val;
return;
}
if(/^(export\s+)?API_KEY=/.test(fix)){ fs['__api_key'] = true; return; }
}
// Runs one of the demo commands against simulated state. Returns {ok, out}.
function runCommand(cmd){
if(cmd === 'cat needs-fix.txt'){
if(fs['needs-fix.txt'] !== undefined) return {ok:true, out: fs['needs-fix.txt']};
return {ok:false, out: 'cat: needs-fix.txt: No such file or directory'};
}
if(cmd === 'curl api.example.com'){
if(fs['__api_key']) return {ok:true, out: '{"status":"ok"}'};
return {ok:false, out: 'curl: (401) Unauthorized — missing API_KEY'};
}
if(cmd.startsWith('echo ')){
return {ok:true, out: cmd.slice(5)};
}
return {ok:false, out: cmd + ': command not found'};
}
function tryCommand(cmd){
if(p[cmd]) applyFix(p[cmd]);
const res = runCommand(cmd);
if(res.ok){
print(res.out, 'ok');
return;
}
print(res.out, 'err');
print(`[LEARN] "${cmd}" failed. Enter a fix (blank to give up):`, 'learn');
awaitingFix = cmd;
}
form.addEventListener('submit', function(e){
e.preventDefault();
const val = input.value;
input.value = '';
if(awaitingFix){
print('> ' + (val || '(blank)'), 'dim');
if(!val.trim()){
print(`f: giving up on "${awaitingFix}"`, 'err');
awaitingFix = null;
return;
}
p[awaitingFix] = val.trim();
renderP();
applyFix(val.trim());
const cmd = awaitingFix;
awaitingFix = null;
const res = runCommand(cmd);
print(res.ok ? res.out : res.out, res.ok ? 'ok' : 'err');
if(!res.ok){
print(`[LEARN] "${cmd}" failed. Enter a fix (blank to give up):`, 'learn');
awaitingFix = cmd;
}
return;
}
if(!val.trim()) return;
print('$ f ' + val, 'prompt');
tryCommand(val.trim());
});
document.querySelectorAll('.try button[data-cmd]').forEach(btn => {
btn.addEventListener('click', () => {
input.value = btn.dataset.cmd;
input.focus();
});
});
resetBtn.addEventListener('click', () => {
p = {}; fs = {}; awaitingFix = null;
renderP();
term.innerHTML = '';
print('// memory reset — p is empty again', 'dim');
});
print('// try: cat needs-fix.txt', 'dim');
print('// it will fail once — teach it a fix like: echo hello > needs-fix.txt', 'dim');
})();
</script>
</body>
</html>
+31
View File
@@ -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
}
+55
View File
@@ -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