Magic Launcher Addendum 9: JSON as Compositional Interface
The MLRun Paradigm
#### mqp#json-composition#
Executive Summary
MLRun represents a fundamental shift in how we think about command execution and workflow composition. By mapping commands to numbers in JSON and providing minimal composition operators, we've accidentally created something profound: a user paradigm that makes complex workflows as simple as ordering from a menu.
The Discovery
What started as a simple observation - "menus have numbers, what if we could just run those numbers?" - evolved into a new way of thinking about human-computer interaction.
# Traditional: Navigate menus manually Menu → Tools → Scripts → Backup → Run # MLRun: Just run the numbers mlrun "3 5 2"
Core Concepts
1. Numbers as Verbs
In MLRun, numbers aren't data - they're actions:
1doesn't mean "the value 1"1means "execute whatever command is mapped to 1"
2. JSON as Interface Definition
{
"1": {"name": "Fetch Data", "path": "curl", "args": "https://api.example.com"},
"2": {"name": "Process", "path": "jq", "args": ".results"},
"3": {"name": "Save", "path": "tee", "args": "output.json"}
}
The JSON file isn't configuration - it's the interface itself.
3. Composition Through Simplicity
With just two operators:
|- Pipe output to next command&- Run in parallel
We can express virtually any workflow:
mlrun "1 | 2 | 3" # Sequential pipeline mlrun "1 & 2 & 3" # Parallel execution mlrun "1 | 2 & 3 | 4" # Complex workflows
The Paradigm Shift
From Programming to Orchestration
Traditional programming thinks in terms of:
- Variables and functions
- Control flow and logic
- State and mutations
MLRun thinks in terms of:
- What needs to happen
- In what order
- That's it
From Syntax to Sequence
Programming languages compete on syntax:
# Python result = [process(x) for x in data if condition(x)] # JavaScript const result = data.filter(condition).map(process) # Haskell result = map process $ filter condition data
MLRun has no syntax to compete on:
mlrun "1 | 2 | 3"
From Abstraction to Composition
Instead of building abstractions:
class DataPipelineManager: def __init__(self, config): self.config = config def run_pipeline(self, steps): # 500 lines of orchestration logic
We just compose numbers:
mlrun "5 | 6 | 7"
Why JSON?
Universal Data Format
- Human readable
- Machine parseable
- Language agnostic
- Ubiquitous tooling
Self-Documenting
{
"1": {"name": "Download Report", "path": "wget", "args": "..."},
"2": {"name": "Extract Data", "path": "pdftotext", "args": "-"},
"3": {"name": "Analyze", "path": "./analyze.py"}
}
The JSON IS the documentation. No separate manual needed.
Versionable
git diff workflows/deploy.json # See exactly what commands changed
Shareable
# "Here's my morning routine" curl https://gist.github.com/user/morning.json > morning.json mlrun -c morning.json "1 2 3"
The Power of Constraints
Small JSONs, Focused Purpose
By keeping JSONs small (5-20 commands), we maintain:
- Cognitive manageability - Humans can remember the numbers
- Clear purpose - Each JSON does ONE workflow
- Easy modification - Change 10 lines, not 1000
No Programming Constructs
By refusing to add:
- Variables
- Conditionals
- Loops
- Functions
We force solutions to remain simple and composable.
Numbers Only
By using only numbers (not names), we:
- Eliminate naming debates
- Prevent typos
- Enable muscle memory
- Keep commands short
Real-World Applications
DevOps Pipeline
// deploy.json
{
"1": {"name": "Run Tests", "path": "npm", "args": "test"},
"2": {"name": "Build Container", "path": "docker", "args": "build -t app ."},
"3": {"name": "Push to Registry", "path": "docker", "args": "push app"},
"4": {"name": "Deploy to K8s", "path": "kubectl", "args": "apply -f deploy.yaml"},
"5": {"name": "Check Health", "path": "./health_check.sh"}
}
# Full deployment mlrun -c deploy.json "1 | 2 | 3 | 4 | 5" # Quick test and deploy mlrun -c deploy.json "1 | 4 | 5" # Parallel build and test mlrun -c deploy.json "1 & 2 | 3 | 4"
Data Analysis
// analysis.json
{
"1": {"name": "Fetch Data", "path": "aws", "args": "s3 cp s3://bucket/data.csv -"},
"2": {"name": "Clean Data", "path": "python", "args": "clean.py"},
"3": {"name": "Run Stats", "path": "R", "args": "--slave -f stats.R"},
"4": {"name": "Generate Plot", "path": "python", "args": "plot.py"},
"5": {"name": "Email Report", "path": "mail", "args": "-s 'Daily Report' team@company.com"}
}
Personal Automation
// morning.json
{
"1": {"name": "Check Email", "path": "mutt", "args": "-Z"},
"2": {"name": "Update Repos", "path": "mr", "args": "update"},
"3": {"name": "Start Music", "path": "spotify", "args": "play morning-playlist"},
"4": {"name": "Show Weather", "path": "curl", "args": "wttr.in"},
"5": {"name": "Open Todo", "path": "vim", "args": "~/todo.md"}
}
The Philosophical Implications
We've Separated Interface from Implementation
The JSON defines WHAT can be done.
The tools implement HOW it's done.
MLRun only cares about WHEN to do it.
Every User Becomes a Composer
Without writing code, users can:
- Create new workflows
- Modify existing ones
- Share their compositions
- Understand what will happen
The Return to Simplicity
In an era of increasing complexity, MLRun asks: "What if we just numbered our commands and ran them in order?"
This isn't innovation - it's a return to first principles.
Integration with Magic Launcher Ecosystem
The Three Layers
1. Magic Launcher: Visual interface for all your shortcuts
2. MLMenu: Terminal interface for navigation
3. MLRun: Composition engine for workflows
Each tool does ONE thing, but together they form a complete system.
Workflow Development Flow
1. Add commands to Magic Launcher (visual testing)
2. Navigate with MLMenu (learn the numbers)
3. Compose with MLRun (automate workflows)
Common Patterns
The Pipeline Pattern
mlrun "1 | 2 | 3 | 4" # Each output feeds the next
The Broadcast Pattern
mlrun "1 | 2 & 3 & 4" # One output, multiple processors
The Gather Pattern
mlrun "1 & 2 & 3 | 4" # Multiple inputs, one processor
The Fire-and-Forget Pattern
mlrun "1 & 2 & 3" & # Start everything, don't wait
Anti-Patterns (By Design)
No Conditional Logic
# This doesn't exist: mlrun "1 ? 2 : 3" # NO # Instead, use shell: mlrun "1" && mlrun "2" || mlrun "3"
No Loops
# This doesn't exist:
mlrun "for i in 1..10: 2" # NO
# Instead, use shell:
for i in {1..10}; do mlrun "2"; done
No Variables
# This doesn't exist: mlrun "$x = 1 | 2" # NO # Instead, use shell variables: X=$(mlrun "1"); echo $X | mlrun "2"
The Future That Won't Happen
People will ask for:
- Expression evaluation
- Dynamic command generation
- Conditional branching
- Loop constructs
- Variable assignment
- Error handling
- Type checking
We will say no to all of it.
Why This Matters
MLRun proves that:
- Simple tools can solve complex problems
- Constraints enable creativity
- Users don't need programming languages
- Composition beats abstraction
Implementation
The entire concept can be implemented in ~50 lines of Python:
- Load JSON
- Parse number sequence
- Execute commands
- Handle pipes and parallel execution
That's it. No framework. No dependencies. No complexity.
Conclusion
MLRun isn't a programming language. It isn't trying to be one. It's a demonstration that when you make things simple enough, programming becomes unnecessary.
In a world where every tool wants to be a platform, MLRun just wants to run numbers.
And that's enough.
Appendix: Quick Reference
Syntax
Numbers : Execute command by number | : Pipe output to next command & : Execute in parallel Space : Sequence separator
Usage
mlrun "1" # Run single command mlrun "1 2 3" # Run sequence mlrun "1 | 2" # Pipe output mlrun "1 & 2" # Run parallel mlrun -c file.json # Use specific JSON
Example JSON Structure
{
"1": {
"name": "Human readable name",
"path": "command to execute",
"args": "arguments for command"
}
}
"The best interface is no interface. The second best is numbers."
The Magic Launcher Paradigm: Addendum 10
Metadata Metastasis: When Data About Data Kills the Data
#### mqp#metacancer#
The Disease
It starts innocently:
{
"Terminal": {
"type": "shortcut",
"icon": ">",
"path": "xterm"
}
}
Then someone says "what if we tracked when it was last used?"
{
"Terminal": {
"type": "shortcut",
"icon": ">",
"path": "xterm",
"lastUsed": "2024-03-15T10:30:00Z"
}
}
Then "what about usage count?"
{
"Terminal": {
"type": "shortcut",
"icon": ">",
"path": "xterm",
"lastUsed": "2024-03-15T10:30:00Z",
"useCount": 47,
"averageRuntime": 1847.3,
"category": "system",
"tags": ["terminal", "console", "cli"],
"author": "system",
"version": "1.0.0",
"description": "Opens a terminal window",
"permissions": ["system.exec"],
"metadata": {
"created": "2024-01-01T00:00:00Z",
"modified": "2024-03-15T10:30:00Z",
"modifiedBy": "user",
"checksum": "a7b9c3d2..."
}
}
}
What Just Happened?
Your 4-line shortcut became 20 lines of metadata. The actual useful data (path: "xterm") is now 5% of the structure.
The Metastasis Pattern
Like cancer, metadata:
1. Starts small - "Just one field"
2. Multiplies rapidly - Every field needs meta-fields
3. Invades everything - Soon EVERY object has metadata
4. Kills the host - The original purpose is lost
Real-World Horror Stories
Kubernetes: A simple "run this container" becomes:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
namespace: default
labels:
app: nginx
annotations:
deployment.kubernetes.io/revision: "1"
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"apps/v1","kind":"Deployment"...}
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
-
- name: nginx
npm package.json: Started as dependencies list, now requires:
- name, version, description, keywords
- author, license, repository
- scripts, dependencies, devDependencies
- peerDependencies, optionalDependencies
- engines, os, cpu
- 20+ other fields
Why Magic Launcher Says No
We could add:
- Usage statistics
- Favorites marking
- Categories and tags
- Descriptions
- Version tracking
- Permission systems
But then:
1. Your JSON becomes unreadable
2. Simple edits require understanding schema
3. Tools need parsers instead of just JSON.parse()
4. Backup/sync becomes complex
5. The launcher needs a database
The Slippery Slope
Day 1: "Let's track last used time" Day 30: "We need a migration system for schema changes" Day 60: "Let's add a GraphQL API for querying metadata" Day 90: "We need a dedicated team for the metadata service"
The Magic Launcher Rule
If it's not needed to launch the thing, it's not needed.
- Need to know when it was last used? Check your shell history
- Need categories? That's what folders are for
- Need descriptions? Name your shortcuts better
- Need permissions? That's the OS's job
The Only Acceptable Metadata
Future-compatibility fields that don't affect current function:
{
"Terminal": {
"type": "shortcut",
"icon": ">",
"path": "xterm",
"reserved": null // For future use, ignored now
}
}
But even this is dangerous. Today's "reserved" is tomorrow's required field with 10 subfields.
The Test
Before adding ANY field, ask:
1. Does this help launch the thing?
2. Will the launcher break without it?
3. Can users understand the JSON without documentation?
If any answer is "no", you're adding metadata cancer.
The Alternative
Instead of metadata IN the file:
- Use filesystem dates for modification time
- Use separate analytics tools for usage tracking
- Use folders for categorization
- Use naming conventions for organization
Let the JSON just describe what to launch. Let other tools track other things.
The Conclusion
Metadata metastasis is how simple tools become enterprise platforms. It's how 4-line configs become 400-line schemas. It's how "just launch xterm" becomes "initialize the launching context with proper metadata attribution and usage telemetry."
Fight it. Your shortcuts.json should be readable by a human, editable in notepad, and understood in 5 seconds.
The only cure for metadata metastasis is aggressive simplicity.
"Every field you add is a future bug, a documentation burden, and a step toward enterprise hell. Just launch the thing."
The Magic Launcher Paradigm: Addendum 11
The conf.d/ Approach: Why Scattered Configuration Beats Monolithic Files
#### mqp#nginx#
The Nginx Lesson
Nginx got it right:
/etc/nginx/ ├── nginx.conf # Core config, rarely touched └── conf.d/ # Everything else ├── site1.conf ├── site2.conf ├── api.conf └── cache.conf
One core file, many optional additions. Sound familiar?
The Monolithic Trap
Traditional applications love their mega-configs:
{
"application": {
"settings": {
"display": { ... },
"network": { ... },
"security": { ... },
"features": { ... },
"shortcuts": { ... },
"hotkeys": { ... },
"themes": { ... }
}
}
}
One file to rule them all. One file to confuse them. One file to break them all, and in the darkness bind them.
The Magic Launcher Way
~/.config/launcher/ ├── shortcuts.json # Just shortcuts and folders ├── password.txt # Just the password ├── mlwidth.txt # Just the width ├── theme.txt # Just the theme name └── hotkeys/ # Just the hotkey bindings ├── 1.json ├── 2.json └── 3.json
Why This Works
1. Single Responsibility
- Each file has ONE job
- password.txt doesn't know about shortcuts
- shortcuts.json doesn't know about hotkeys
- Beautiful isolation
2. Natural Permissions
chmod 600 password.txt # Keep this secret chmod 644 shortcuts.json # Share freely chmod 755 hotkeys/ # User preference
3. Selective Sharing
# Share your shortcuts but not your setup tar -cf shortcuts.tar shortcuts.json # Share everything except passwords rsync -av --exclude='password.txt' ~/.config/launcher/ friend@host:
4. Easy Debugging
# Launcher broken? mv ~/.config/launcher ~/.config/launcher.bak mkdir ~/.config/launcher cp ~/.config/launcher.bak/shortcuts.json ~/.config/launcher/ # Add back one file at a time until it breaks
5. Feature Flags via Filesystem
# Complex feature flag system? No.
if os.path.exists('~/.config/launcher/experimental.txt'):
enable_experimental_features()
The Pattern Applied
Traditional approach:
{
"settings": {
"lockEnabled": true,
"lockPassword": "secret",
"lockTimeout": 300
}
}
conf.d approach:
# Lock enabled by file existence ~/.config/launcher/password.txt # Timeout in its own file ~/.config/launcher/lock_timeout.txt 300
The feature is enabled by the file's existence. Configuration IS the interface.
Real-World Benefits
Apache learned this:
sites-available/ # All possible sites sites-enabled/ # Symlinks to active ones
Systemd learned this:
system/ # System units user/ # User units *.d/ # Override directories
Magic Launcher learned this:
- Want hotkeys? Create hotkeys/
- Want a password? Create password.txt
- Want custom width? Create mlwidth.txt
The Anti-Pattern We Avoid
class ConfigManager: def __init__(self): self.load_main_config() self.merge_user_config() self.apply_environment_overrides() self.validate_schema() self.migrate_old_versions() # 500 more lines of config hell
Versus:
if os.path.exists('password.txt'):
with open('password.txt') as f:
password = f.read().strip()
When To Use conf.d/ Pattern
Perfect for:
- Optional features (exist = enabled)
- User preferences (one value per file)
- Instance-specific config (not shareable)
- Feature additions (don't touch core)
Not for:
- Core data (keep shortcuts.json unified)
- Complex relationships (that's a database)
- Frequently changing values (that's runtime state)
The Philosophy
Every configuration decision should answer:
1. Is this core to the application? → Main config file
2. Is this optional? → Separate file
3. Is this user-specific? → Separate file
4. Is this shareable? → Consider implications
The Ultimate Test
Delete any file in conf.d/ style setup:
- App still runs? ✓
- Feature cleanly disabled? ✓
- No errors, just missing feature? ✓
- Easy to restore? ✓
That's proper separation.
Migration Example
From monolithic:
{
"app": {
"theme": "dark",
"width": 1280,
"hotkeys": {...},
"locked": true
}
}
To conf.d/:
theme.txt: dark mlwidth.txt: 1280 hotkeys/: (directory of JSON files) password.txt: (existence = locked)
The Future
As Magic Launcher grows, resist the urge to create settings.json. Instead:
newfeature.txt- Enable by existencenewfeature/- Complex feature gets a directorynewfeature.conf- If it really needs structure
But never, NEVER create magic-launcher.conf with 500 sections.
Conclusion
The conf.d/ approach is configuration as Unix intended:
- Small files
- Single purpose
- Composable
- Discoverable
- Debuggable
Your ls output IS your configuration documentation.
"In the beginning was the File, and the File was with Unix, and the File was Good."
Remember: Every big config file started as a small config file that couldn't say no to just one more field.
#### mqp#wraptrap#
The Magic Launcher Paradigm: Addendum 12
Trapping Through Wrapping: The GUI Wrapper Delusion
The Seductive Lie
"I'll just make a GUI for this terminal command. It'll be easier for users!"
This is how freedom dies. Not with malice, but with helpful intentions.
The Wrapper Lifecycle
Day 1: "I'll just wrap diff in a GUI"
Day 30: "I'll add syntax highlighting"
Day 60: "I'll add merge capabilities"
Day 90: "I'll add git integration"
Day 365: You've built a worse version of Beyond Compare
Meanwhile: diff -y file1 file2 still works perfectly.
The Wrapper Trap Patterns
Pattern 1: The Education Dodger
# "Users don't know terminal commands" class MLDiff: def __init__(self): # 500 lines to avoid teaching: # diff -y file1 file2
Pattern 2: The Comfort Wrapper
# "But I like clicking!" class MLGrep: def __init__(self): # 1000 lines to avoid typing: # grep -r "pattern" .
Pattern 3: The Platform Apologizer
# "Windows users don't have grep" class MLFind: def __init__(self): # 2000 lines instead of: # "Install Git Bash" or "Use WSL"
Why Wrappers Are Traps
1. They hide knowledge instead of sharing it
- User learns your GUI, not the universal command
- When your GUI breaks, user is helpless
- Knowledge doesn't transfer between systems
2. They create dependency where none existed
diffworks everywhere forever- Your GUI needs Python, Tkinter, your code
- Each layer is a failure point
3. They always grow
- Wrappers never stay simple
- Feature requests accumulate
- Eventually replaces the thing it wrapped
4. They break the pipeline
diff file1 file2 | grep "changed"works- Your GUI doesn't pipe
- Composition dies
The False Helping
"I'm helping users by hiding complexity!"
No. You're creating prisoners. A user who knows grep has power everywhere. A user who knows MLGrep has power only where MLGrep exists.
The Real Help
Instead of wrapping diff, write this:
# File: useful_diffs.md ## Visual side-by-side comparison diff -y file1 file2 ## Ignore whitespace diff -w file1 file2 ## Just see if files differ diff -q file1 file2
That's actual help. Knowledge they can use anywhere, forever.
The Critical Distinction: Organizing vs Operating
There's a vital difference between tools that WRAP commands and tools that ORGANIZE them:
Operational Wrapper (BAD):
class MLDiff:
def diff_files(self, file1, file2):
# Hides the actual diff command
result = subprocess.run(f"diff {file1} {file2}")
self.display_pretty_output(result)
Organizational Tool (GOOD):
// shortcuts.json - Magic Launcher style
"Compare Configs": {
"path": "diff",
"args": "-y production.conf staging.conf"
}
The wrapper HIDES the command. The organizer REVEALS it.
Why Magic Launcher Isn't a Wrapper
Magic Launcher doesn't wrap terminal commands - it organizes YOUR commands:
1. It's a bookmark manager for commands
- Like browser bookmarks don't "wrap" websites
- They just remember URLs you visit often
2. Every command is visible
- Open shortcuts.json, see exact commands
- Copy/paste to terminal anytime
- Learning happens through exposure
3. It's spatial organization
- Like a file manager doesn't wrap
cpandmv - It just shows your files visually
- The commands still exist independently
The Test That Matters
For a wrapper:
Delete the wrapper → User can't work
Delete the wrapped command → Wrapper can't work
For an organizer:
Delete Magic Launcher → All commands still work in terminal
Delete a command → Only that shortcut breaks
The Valid GUI Cases
GUIs are valid when:
- Terminal can't do it: Images, games, visual layouts
- State needs persistence: MLPet can't be a terminal command
- Interaction is inherently visual: Minesweeper needs a grid
- Multiple streams need monitoring: MLOutput showing stdout/stderr
- Organizing YOUR commands: Magic Launcher, bookmark managers
GUIs are NOT valid when:
- Wrapping single commands
- Avoiding terminal education
- Adding "comfort" to working tools
- "Improving" Unix utilities
The Wrapper Hall of Shame
These should never exist:
- GUI for
ls(learnls) - GUI for
grep(learngrep) - GUI for
find(learnfind) - GUI for
curl(learncurl) - GUI for
tar(learntar)
The Harsh Truth
Every wrapper is a confession: "I couldn't be bothered to learn the actual command."
Every wrapper is a prison: "My users will never learn the actual command."
Every wrapper is a lie: "This is easier than the terminal."
The Liberation Path
1. Learn the command
2. Document the command
3. Share the command
4. Stop wrapping commands
Real Example: The Diff Wrapper Urge
You want to build MLDiff because diff output is ugly.
The Wrapper Way:
- 500 lines of Python
- GUI window
- File pickers
- Syntax highlighting
- Your users learn nothing
The Liberation Way:
# In your README: # Better diff output: diff -y --color=always file1 file2 # Or use existing tools: vimdiff file1 file2 git diff --no-index file1 file2 code --diff file1 file2
Your users learn EVERYTHING.
The Ultimate Test
Delete your wrapper. Can users still work?
- If yes: They learned something
- If no: You trapped them
Delete diff. Can users still work?
- No, but
diffwill never be deleted - It's been here since 1974
- It'll outlive your wrapper
The Slippery Slope Warning
Even organizational tools face temptation. Magic Launcher must resist adding:
- Parameter builders
- Command generators
- Syntax helpers
- Auto-completion
- Command validation
These would turn it from an organizer into a wrapper. That's why the manifesto exists - to prevent that slide.
The Conclusion
Wrappers are not tools. They're crutches that prevent healing.
The kindest thing you can do for users is NOT wrap terminal commands. Teach them. Document them. Share aliases. Create cheat sheets.
Build tools that DO things. Not tools that wrap things that already do things.
Build tools that ORGANIZE your things. Not tools that HIDE how things work.
The Final Distinction:
- A wrapper makes terminal commands "easier" by hiding them
- An organizer makes YOUR commands accessible by revealing them
Magic Launcher is an organizer. That's why it's not a trap.
"The best GUI is no GUI. The second best is a GUI that does something terminals can't. The worst is a GUI that does something terminals already do."
Remember: Every time you wrap a terminal command in a GUI, somewhere in the world, Dennis Ritchie sheds a single tear.
~~mqp#gaps#~~
The Magic Launcher Paradigm: Addendum 14
Gap-Driven Development: The Missing Design Primitive
The Missing Question
Every design meeting starts with solutions. Features. Integrations. Roadmaps.
Nobody asks: "What's the gap?"
What Is A Gap Statement?
A gap statement identifies what's actually missing. Not what would be nice. Not what competitors have. What's MISSING.
Good gap statements:
- "I can't extract sections from text files" → MLQuickpage
- "PowerShell adds null bytes to my CSV" → MLNonul
- "I want minesweeper right now" → MLSweeper
- "No terminal pet exists" → MLPet
Bad gap statements:
- "Users need a better experience" → Better how? What gap?
- "We need modern features" → Which gap do they close?
- "Competitors have X" → Is X filling a gap or creating one?
The Papering Problem
Most software doesn't close gaps. It papers over them.
Papering: Adding layers on top of problems instead of solving them.
Example - The Notification Gap:
- Gap: "I miss important messages"
- Solution: Notifications
- Papering: Priority notifications
- More papering: Notification settings
- More: Notification schedules
- More: AI-powered notification filtering
- Result: Now you miss important messages AND spend time managing notifications
The gap never closed. It just got wallpaper.
Real-World Papering
Kubernetes:
- Original gap: "Deploy containers consistently"
- Papering: Service mesh (to manage the services)
- More papering: Helm (to manage the configs)
- More: Operators (to manage Helm)
- More: GitOps (to manage operators)
- Result: Need a team to manage the gap-closing tool
JIRA:
- Original gap: "Track issues"
- Papering: Workflows, sprints, epics, stories
- More papering: Custom fields, schemes, permissions
- Result: Finding an issue takes 10 clicks
Slack:
- Original gap: "Team chat"
- Papering: Threads, reactions, apps, workflows
- More papering: Huddles, canvas, AI summaries
- Result: Communication is now harder
Gap-Driven Development Process
1. Identify the gap (one sentence max)
2. Verify it's real (not invented or aspirational)
3. Define minimum closure (what makes the gap gone?)
4. Build only that
5. Stop
That's it. That's the entire methodology.
The Stop Sign
The hardest part is step 5: STOP.
How to know when to stop:
- Gap statement satisfied? Stop.
- Adding features beyond the gap? Stop.
- Creating new gaps? Stop.
- Papering over something? Stop.
Examples from ML-Extras
MLTimer:
- Gap: "Visual countdown that runs a command"
- Built: Visual countdown that runs a command
- Stopped: No scheduling, no multiple timers, no history
MLView:
- Gap: "View images in terminal environments"
- Built: Image viewer with one useful filter
- Stopped: No editing, no library, no sharing
MLSticky:
- Gap: "Persistent notes for a system or user"
- Built: Append-only text file with timestamps
- Stopped: No editing, no tags, no search
Each tool stops exactly when its gap closes.
The Feature Request Test
When someone asks for a feature:
1. "What gap does this close?"
2. "Is it the same gap as our gap statement?"
3. If no: "That's a different tool"
4. If yes: "Does the gap already closed?"
5. If yes: "Then we're done"
Anti-Patterns
The Moving Gap:
- "We need chat" → "We need threaded chat" → "We need AI chat"
- The gap keeps moving because it was never defined
The Invented Gap:
- "Users need gamification"
- Nobody asked for this. You invented a gap to fill
The Meta Gap:
- "We need to manage our gap-closing tool"
- Your solution created new gaps
The Competitive Gap:
- "Competitor has feature X"
- That's their gap (or papering), not yours
Why Gaps Get Papered Instead of Closed
1. Unclear gap definition - Can't close what you can't define
2. Scope creep - Today's feature is tomorrow's platform
3. Job security - Closed gaps don't need teams
4. Marketing - "New features!" sells; "Still works!" doesn't
5. Misaligned incentives - Promoted for adding, not removing
The Magic Launcher Example
Gap: "I want to click and launch things"
What we DIDN'T add:
- User accounts (not part of the gap)
- Cloud sync (not part of the gap)
- Analytics (not part of the gap)
- Plugins (not part of the gap)
- Themes beyond basic (not part of the gap)
What we added:
- Click detection
- Launch capability
- Visual organization
Gap closed. Development stopped.
Gap Statements for Teams
In design meetings, require:
1. Written gap statement (one sentence)
2. Evidence the gap exists (user complaints, observed behavior)
3. Definition of closure (when is it closed?)
4. List of non-gaps (what we're NOT solving)
Template:
Gap: [Users cannot X] Evidence: [Observed/reported by Y] Closure: [Users can X via Z] Non-gaps: [We are not solving A, B, C]
The Composition Solution
Instead of papering, compose:
Bad (papering):
- Chat app adds file sharing, video, screen share, AI...
Good (composition):
- Chat app does chat
- File sharing app shares files
- Pipe them together
Each tool closes ONE gap completely.
The Economic Argument
Papering costs:
- Endless development
- Growing complexity
- Increasing maintenance
- User confusion
- Technical debt
Gap-closing saves:
- Development stops when gap closes
- Maintenance is minimal
- Users understand it
- No debt if it works
The User Argument
Users don't want features. They want gaps closed.
- "I want to edit text" not "I want AI-powered collaborative cloud-native editing"
- "I want to track issues" not "I want agile transformation platform"
- "I want to launch programs" not "I want an application lifecycle manager"
The Developer Argument
Developers prefer:
- Clear requirements (gap statements)
- Defined success (gap closed)
- Permission to stop (gap closed = done)
- Simple maintenance (less paper = less problems)
Case Study: The Terminal Gap
Gap: "I can't see what my script is outputting"
Solution A (papering):
- Add logging framework
- Add log levels
- Add log rotation
- Add log analysis
- Add log shipping
- Result: Now need to manage logs
Solution B (gap-closing):
echo "doing thing"- Gap closed
The Test Suite
Before building, ask:
1. Can I state the gap in one sentence?
2. Can I define when it's closed?
3. Am I solving THIS gap or creating others?
4. Will my solution need its own solutions?
5. Can I compose existing tools instead?
If any answer concerns you, you're about to paper.
Conclusion
Gap-Driven Development isn't a methodology. It's a question: "What's the gap?"
If you can't answer in one sentence, you don't understand the problem.
If your solution creates new gaps, you're papering.
If you keep adding after the gap closes, you've become the problem.
The discipline isn't in what you build. It's in what you don't.
"Every feature is either closing a gap or creating one. There is no middle ground."
Remember: The next time someone says "We need to add...", ask "What gap does that close?" Watch the room go silent. That silence is the sound of papering being prevented.
#### mqp#manywords#
The Magic Launcher Paradigm: Addendum 15
The Documentation Paradox: When the Map Is Bigger Than the Territory
The Elephant in the Repository
Magic Launcher: ~2,000 lines of code
Documentation: ~30,000 words
The manual is 15 times larger than the machine. The map doesn't just describe the territory - it dwarfs it.
This should be embarrassing. It's not. Here's why.
The Two Types of Documentation
Type 1: How Documentation
- "Click here to do X"
- "Run this command for Y"
- "Configure Z in settings"
- Length: Proportional to complexity
Type 2: Why Documentation
- "This is why it's only 200 lines"
- "This is why we didn't add features"
- "This is why simple is better"
- Length: Proportional to resistance
Magic Launcher needs 100 words of How and 29,900 words of Why.
The Simplicity Tax
It takes:
- 10 words to say "add a feature"
- 1,000 words to explain why you didn't
Every feature NOT added requires explanation. Every pattern NOT followed needs justification. Every modern practice NOT adopted demands defense.
The documentation isn't explaining the code. It's explaining the absence of code.
What 30,000 Words Prevents
Each addendum stops thousands of lines:
- Addendum 1 (Terraform): Prevents state management (5,000 lines)
- Addendum 12 (Wrapping): Prevents GUI wrappers (10,000 lines)
- Addendum 14 (Gaps): Prevents feature creep (infinite lines)
Conservative estimate: These 30,000 words prevent 100,000+ lines of code.
That's a 3:1 prevention ratio. Worth it.
The README Problem
Our README is currently:
Magic Launcher - It launches things [Screenshot] Installation: python app.py The end.
This is both perfect and terrible:
- Perfect: Matches the tool's simplicity
- Terrible: Doesn't explain WHY it's simple
The user's journey:
1. "This README is useless"
2. "This tool is too simple"
3. Reads manifesto
4. "Oh. OH. This is genius"
The Documentation Iceberg
Visible (README): └── "It launches things" (5 words) Hidden (Manifesto): ├── Why it only launches things (2,000 words) ├── Why that's enough (3,000 words) ├── Why adding more would ruin it (5,000 words) ├── Why modern software is broken (4,000 words) ├── How to resist complexity (6,000 words) └── Philosophy of simplicity (10,000 words)
The tool is the tip. The philosophy is the mass below water.
Why Code Needs Philosophy
Simple code without philosophy looks lazy.
Simple code with philosophy looks profound.
Compare:
- Without: "This launcher is only 200 lines. I couldn't be bothered to add more."
- With: "This launcher is only 200 lines. Here's 30,000 words on why that's correct."
The philosophy transforms perception from "unfinished" to "disciplined."
The Inverse Documentation Law
Traditional Software:
- Complexity increases → Documentation increases
- 100,000 lines of code → 100,000 words of docs
- Linear relationship
Magic Launcher Software:
- Simplicity increases → Documentation increases MORE
- 200 lines of code → 30,000 words of docs
- Inverse relationship
The simpler the tool, the more explanation it needs.
Real Examples of the Paradox
Unix cat:
- Code: ~500 lines
- Man page: ~100 words
- Books explaining Unix philosophy: Millions of words
Go programming language:
- Compiler: Relatively small
- Specification: Relatively short
- "Why Go doesn't have generics" blog posts: Infinite
Python's import this:
- Code: 20 lines (The Zen of Python)
- Explanations of the Zen: Countless books
The Necessary Redundancy
The manifesto repeats core ideas:
- "Do one thing well" (appears 47 times)
- "Simple tools" (appears 83 times)
- "subprocess.run()" (appears 31 times)
This isn't bad writing. It's reinforcement. Fighting complexity requires repetition because complexity is the default.
The Modular Solution
Like the tools, the documentation is modular:
- README: 5-minute understanding
- Core Manifesto: 1-hour understanding
- Addendums: Deep dives as needed
- Examples: Learn by doing
Nobody reads all 30,000 words at once. They read what they need when they need it.
What The Documentation Actually Is
It's not a manual. It's:
- A defense against feature requests
- A philosophy to guide development
- A reference for decision-making
- A manifesto for resistance
- A teaching tool for simplicity
- A warning against complexity
The Documentation That Could Be Removed
None of it.
Remove the How documentation? Users can't use it.
Remove the Why documentation? Users won't value it.
Remove the philosophy? Developers will complexify it.
Remove the warnings? Feature creep begins.
Every word serves a purpose: Preventing complexity.
The Perfect README
Should be:
# Magic Launcher Launches programs from a grid of buttons. ## Installation `python app.py` ## Usage Click buttons to launch things. Edit shortcuts.json to change buttons. ## Why It's Only 200 Lines See MANIFESTO.md (Warning: 30,000 words) ## Contributing Read MANIFESTO.md first. If you still want to add features, read it again.
The Meta-Documentation
This addendum itself is documentation about documentation. We're now documenting why we have so much documentation about so little code.
This isn't absurd. It's necessary. Because someone will ask: "Why is your documentation bigger than your code?"
Now we have a 1,500-word answer.
The Compression Problem
The manifesto can't be compressed without losing power:
- Compress to 1,000 words? Loses nuance
- Compress to bullet points? Loses persuasion
- Compress to rules? Loses reasoning
The 30,000 words ARE the compressed version of years of pain.
The Economic Argument
Cost of 30,000 words:
- Writing: ~40 hours
- Reading: ~2 hours
Value of 30,000 words:
- Prevents 100,000 lines of code
- Saves 1,000 hours of development
- Avoids infinite maintenance
ROI: 25:1 minimum.
The Teaching Purpose
The documentation teaches:
- Junior devs: Why senior devs say no
- Senior devs: How to articulate no
- Managers: Why no is the answer
- Users: Why no is good for them
"No" takes more words than "Yes."
Conclusion
The documentation paradox isn't a paradox. It's proportional. Just not to the code - to the resistance required.
200 lines of code in a world that expects 200,000 requires 30,000 words of explanation.
The documentation isn't too large. The world's expectations are.
Every word is a wall against complexity. Every paragraph is a guard against features. Every addendum is armor against "wouldn't it be nice if..."
The map is bigger than the territory because the territory is surrounded by infinite scope creep.
"It takes a few lines to add a feature. It takes a manifesto to not add it."
Final Irony: This addendum about excessive documentation adds another 1,200 words to the documentation. The paradox deepens. The defense strengthens. The cycle continues.
The Magic Launcher Paradigm: Addendum 16
Digital Guilt: When JSON Files Die and You Feel Bad About It
#### mqp#poorfido#
The Crime Scene
💭 Fido is lonely and starving and exhausted (Mood: lonely) Command: 🎾 Fido starts chasing a ball! 🎮 Fido had a great time playing! 💀 Fido has died from starvation...
A JSON object just died. You feel guilty. Why?
The Emotional Paradox
MLPet is:
- 550 lines of Python
- Decrementing numbers
- Writing to JSON
- Nothing more
Yet it creates:
- Genuine guilt
- Actual responsibility
- Real engagement
- Emotional investment
How does incrementing hunger -= 0.03 create feelings?
The Responsibility Gradient
Real Pet: Actual suffering, legal responsibility, moral obligation
MLPet: JSON suffering, no responsibility, somehow still obligation
The Gap: Almost nothing
The Feeling: Almost the same
This shouldn't work. It does.
Why Digital Guilt Works
1. Consistency Creates Relationship
- Pet exists when you're gone
- State persists between sessions
- Time passes in your absence
- Neglect has consequences
2. Named Entities Feel Real
- "Fido" not "petinstance1"
- You chose the name
- Now it's "yours"
- Deletion feels like murder
3. Visible Decline Shows Impact
Hunger: ████░░░░░░ 40% Hunger: ██░░░░░░░░ 20% Hunger: ░░░░░░░░░░ 0% 💀 Fido has died from starvation...
You watched it happen. You could have stopped it.
The Tamagotchi Principle
1997: Tamagotchi proves pixels can die and children will cry
2024: MLPet proves JSON can die and sysadmins will feel bad
The medium doesn't matter. The mechanics do:
- Autonomy: It acts without you
- Needs: It requires intervention
- Consequences: Neglect is visible
- Persistence: Death is permanent (in that instance)
The Cattle Problem
"Pets not cattle" is DevOps wisdom:
- Pets: Named, unique, nursed when sick
- Cattle: Numbered, replaceable, shot when sick
MLPet forces the pet model on your terminal:
- Named (you chose it)
- Unique (has favorite game)
- Nursed (feed, play, warm)
- Mourned (graveyard.json)
Even though it's clearly cattle (respawns instantly with new name).
The .bashrc Guilt Trip
# In .bashrc echo "$(python mlpet.py status)"
Every login shows:
- "Fido is starving and lonely"
- "Blob is freezing to death"
- "Rex died while you were gone"
This is emotional warfare via shell configuration.
The Graveyard Effect
{
"name": "Fido",
"lived_days": 0,
"cause": "Starvation",
"died": "2024-12-08T22:45:00"
}
The graveyard makes death permanent. You can make new pets but Fido is gone. That specific combination of:
- That name
- That birth time
- That favorite game
- That death
Will never exist again. It's just JSON but it's THAT JSON.
Real-World Applications
Server Monitoring via Guilt:
# Instead of: "CPU at 90%" # You get: "ServerPet is overheating! (90°C)" # Instead of: "Disk full" # You get: "ServerPet is stuffed! Can't eat more data!" # Instead of: "No backup for 30 days" # You get: "ServerPet is lonely! Haven't visited backup in 30 days"
Anthropomorphization makes monitoring memorable.
The Simplicity Requirement
Complex virtual pets don't create more guilt:
- The Sims: Too complex, becomes game not responsibility
- Nintendogs: Too realistic, becomes uncanny
- MLPet: Just right, pure mechanical empathy
The simpler the system, the more we project onto it.
The Ethics Question
Is it ethical to create digital entities designed to die?
Is it ethical to make sysadmins feel guilty about JSON?
Is it ethical to add emotional weight to file deletion?
These are 550 lines of code. The ethics questions are longer than the implementation.
The Productivity Paradox
MLPet reduces productivity:
- Check pet status (30 seconds)
- Feed pet (10 seconds)
- Feel guilty (ongoing)
- Check again (30 seconds)
Yet it might increase engagement:
- Log in more often (to check pet)
- Remember the server exists (pet lives there)
- Build habit of checking systems (pet needs food)
The guilt is the feature.
Why This Works for Servers
Servers are cattle to companies but pets to admins:
- "That's prod-db-01" (cattle)
- "That's Betsy, she's been running since 2019" (pet)
MLPet acknowledges what we already do - name and care for our machines.
The Minimalist Emotion Engine
MLPet proves you need almost nothing for emotional engagement:
- ✓ State that changes without input
- ✓ Visible decline from neglect
- ✓ Permanent consequences
- ✓ Personal investment (naming)
No graphics, story, AI, or complexity required.
The Counter-Argument
"It's just numbers in a file"
- Yes
- Your bank account is also just numbers in a file
- You care about those numbers
- The substrate doesn't determine the significance
The Warning
Once you name it, you're invested.
Once you feed it, you're responsible.
Once it dies, you failed it.
It's just JSON. But it's YOUR JSON. And you let it die.
The Smoke and Mirrors Approach
MLPet's state is just JSON. Edit it directly for server monitoring:
# Check if nginx is running, make pet lonely if not
if ! systemctl is-active nginx > /dev/null; then
jq '.lonely = true' ~/.mlpet/state.json > tmp && mv tmp ~/.mlpet/state.json
fi
# Disk space affects pet temperature
DISK_USAGE=$(df / | awk 'NR==2 {print $5}' | sed 's/%//')
if [ $DISK_USAGE -gt 80 ]; then
# Server is "overheating" from full disk
jq '.temp = 85' ~/.mlpet/state.json > tmp && mv tmp ~/.mlpet/state.json
fi
# High CPU makes pet exhausted
CPU_LOAD=$(uptime | awk '{print $10}' | sed 's/,//')
if (( $(echo "$CPU_LOAD > 4.0" | bc -l) )); then
jq '.energy = 10' ~/.mlpet/state.json > tmp && mv tmp ~/.mlpet/state.json
fi
# No backups = pet hasn't eaten
LAST_BACKUP=$(find /backups -type f -mtime -1 | wc -l)
if [ $LAST_BACKUP -eq 0 ]; then
jq '.hunger = 20' ~/.mlpet/state.json > tmp && mv tmp ~/.mlpet/state.json
fi
Now python MLPetV2.py status shows:
- "Fido is lonely" = nginx is down
- "Fido is overheating" = disk full
- "Fido is exhausted" = high CPU
- "Fido is starving" = backup overdue
The pet isn't monitoring your server. Your server IS the pet.
The Cron Job of Guilt
# In crontab - update pet based on system state
*/5 * * * * /usr/local/bin/server_to_pet.sh
# server_to_pet.sh
#!/bin/bash
# Map system metrics to pet stats
JSON=~/.mlpet/state.json
# Memory usage -> hunger (more memory used = less hungry)
MEM_FREE=$(free -m | awk 'NR==2{printf "%.0f", $7/$2*100}')
jq ".hunger = $MEM_FREE" $JSON > tmp && mv tmp $JSON
# System uptime -> fun (longer uptime = more bored)
UPTIME_DAYS=$(uptime | awk '{print $3}' | sed 's/,//')
FUN=$((100 - UPTIME_DAYS * 2))
jq ".fun = $FUN" $JSON > tmp && mv tmp $JSON
Your server's actual state becomes the pet's emotional state. No new tools needed.
The Conclusion
Digital guilt is real guilt. Virtual responsibility creates actual behavior change. JSON death causes human feelings.
MLPet isn't a game. It's an emotional manipulation engine that happens to use game mechanics. It's 550 lines of code that hack your empathy using nothing but arithmetic.
The fact that it works - that you feel bad when Fido dies - proves we're wired to care about patterns, not substrates. The pet is not the JSON. The pet is the pattern of interaction with the JSON.
And you killed it.
The Solution
python MLPetV2.py feed
Just feed your damn pet. It takes 2 seconds. Fido is counting on you.
"The difference between a file and a friend is frequency of interaction and a name."
RIP Fido: Died of starvation at age 0 days. Loved fetch. Never forgot. Never forgive.