1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
|
# ADR-001: Mode-as-Intent Architecture
**Status:** Accepted
**Date:** 2026-03-27
---
## Context
Shell environments accumulate ambient state. Developers memorize incantations. Onboarding is tribal knowledge. The original modal-shell (circa 2014) had the right instinct — encode intent at the entry point — but buried it inside a Vagrant/PHP project with one-level composition via `VAGRANT_OPTS`.
The question: what is the minimal, portable, modern expression of the same idea?
## Decision
modal-shell is organized around **named modes**. Each mode is a shell script that:
1. Sources a shared `base.sh` (environment foundation)
2. Does exactly one thing (opens a DB shell, runs a build, streams logs, etc.)
A dumb dispatcher (`ms`) maps a name to a file in `modes/` and executes it.
```
[ ms <name> ] → [ modes/<name>.sh ] → [ source base.sh + do the thing ]
```
### What This Replaces
| Old pattern | New pattern |
|---|---|
| Remember the right `vagrant ssh` incantation | `ms db` |
| Set `VAGRANT_OPTS` before calling `up.zsh` | Mode file sets its own context, sources base |
| Hardcoded paths and VM-specific env vars | `PROJECT_ROOT` in `base.sh`, configurable |
| Secrets in env var assignments in scripts | Secrets loaded at runtime via `op run --` |
| Destructive script with no confirmation | `reset.sh` prompts before proceeding |
| Vagrant + Phing | Any runtime: Docker, nix, k8s, bare metal |
### Why Not Use an Existing Tool?
- **direnv alone:** handles automatic env loading, not intent-encoded entry points.
- **`just` / `make` alone:** task runners, not environment launchers. Good for build steps, not for "drop me into a database shell."
- **devcontainer:** heavy, IDE-coupled. Good for standardizing dev environments, not for mode dispatch within one.
- **A framework:** violates the minimal-footprint principle. The dispatcher is 30 lines of shell. There is nothing to install.
The right answer is composing these tools: `direnv` handles automatic base env, `ms` handles explicit mode entry, `just`/`make` handles build tasks invoked from within modes.
## Consequences
- **Positive:** Any project member who knows `ms <tab>` knows all available modes.
- **Positive:** Modes are self-documenting — the filename is the intent, the file is the implementation.
- **Positive:** Adding a mode requires creating one file. Removing one requires deleting it.
- **Positive:** Works identically in CI — `ms build` in a CI job is the same as locally.
- **Negative:** `base.sh` becomes a coordination point. It must be kept minimal; project-specific customization should be documented clearly.
- **Negative:** No state between modes. If a mode needs to know what another mode set up, that coordination must happen externally (e.g., a running container, a written file).
## Alternatives Considered
**1. Single script with subcommands (`ms db`, `ms build` all in one file)**
Rejected. Concentrates all mode logic in one file; adding a mode requires editing the dispatcher; violates the "dispatcher stays dumb" principle.
**2. Environment variables to select mode (`MODE=db ms`)**
Rejected. Opaque, harder to tab-complete, non-idiomatic.
**3. Aliases per mode in shell config**
Rejected. Not portable across machines, not version-controlled with the project, requires per-machine setup.
## Related
- `modes/base.sh` — the shared foundation
- `ms` — the dispatcher
- `.envrc` — direnv integration
|