Self-healing autopoietic shell: f(), tests, docs, interactive demo, fx fix-source layer
This commit is contained in:
@@ -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.
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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 "$@"
|
||||
}
|
||||
Executable
+9
@@ -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]++'
|
||||
Executable
+11
@@ -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
|
||||
Executable
+23
@@ -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
|
||||
Executable
+49
@@ -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 " \"$@\"; }" }
|
||||
'
|
||||
Executable
+9
@@ -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
|
||||
Executable
+15
@@ -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-
|
||||
@@ -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
@@ -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 <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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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>
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user