board: 29 → in-progress (istos)
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
# 29 — A first run writes local/.env, asking the questions it can't guess
|
||||
|
||||
**Status:** In Progress
|
||||
**Assignee:** istos
|
||||
**Priority:** Medium — nothing is broken without it, but two of bench's
|
||||
defining settings are invisible to anyone who hasn't read `.env.example`
|
||||
**Type:** Feature
|
||||
|
||||
Today a project can run bench for months without ever having a
|
||||
`manager/local/.env`: everything falls back to the defaults in
|
||||
`core/.env.example`, and the two settings that change what bench *is* —
|
||||
claim-on-move and syncing through origin/main — are off unless someone
|
||||
reads the example file and copies it. Nothing tells them the file exists.
|
||||
Make the first run create it: `install.py` (and `start.sh`, which calls
|
||||
it) notices there is no `local/.env`, asks a handful of questions it
|
||||
cannot answer for the project, and writes an `.env` that is a real,
|
||||
commented settings file rather than an empty one.
|
||||
|
||||
## Context
|
||||
|
||||
- `install.py` resolves the adapter and runs its `wire`; it already owns
|
||||
the first-boot story (`first_boot`, `first_boot_clean`) and is the
|
||||
natural place for this. It runs on every `start.sh` and after every
|
||||
`update.sh`, so it must stay idempotent and silent once wired.
|
||||
- `start.sh` calls `python3 install.py || true` and then, only in the
|
||||
port-clash branch, writes a bare `BOARD_PORT=<n>` line into
|
||||
`local/.env`, creating the file if absent. That is the one existing
|
||||
writer and it must keep working on top of whatever setup writes.
|
||||
- `core/.env.example` is the source of truth for what settings exist and
|
||||
what they mean — every key documented in a comment block. The written
|
||||
file should be that file with the answers substituted, not a
|
||||
hand-rolled three-line stub.
|
||||
- `core/config.py` reads `local/.env` with process env > file > default,
|
||||
so a missing key is never an error; this task changes discoverability,
|
||||
not resolution.
|
||||
- Tests live in `tests/`; `test_install_first_boot.py` already exercises
|
||||
`install.py`'s first-run behaviour and is where this belongs.
|
||||
|
||||
**Affected areas:** `install.py`, `start.sh`, `manager/core/.env.example`,
|
||||
`tests/`. No board module changes — nothing in `core/` reads settings
|
||||
differently afterwards.
|
||||
|
||||
## What to build
|
||||
|
||||
- A setup step in `install.py` that runs when `manager/local/.env` does
|
||||
not exist, **after** `first_boot_clean` (see Risks — writing `.env`
|
||||
earlier would flip the first-boot guard and leave the distribution's
|
||||
own cards in a host project).
|
||||
- It asks a short, closed set of questions. Everything else is written at
|
||||
its documented default:
|
||||
- **Solo or team?** Solo → `BOARD_COMMIT_MOVES=` and `BOARD_SYNC=`
|
||||
(today's behaviour). Team → both on, since sync implies
|
||||
commit-moves; the prompt should say in one line what team mode buys
|
||||
(moves claim and commit themselves; boards converge through
|
||||
origin/main) and that it needs a shared `origin`.
|
||||
- **Which agent adapter?** `claude` (default) or `opencode` —
|
||||
enumerated from the adapter directories rather than hardcoded, so a
|
||||
project's own `local/adapters/` shows up.
|
||||
- **What command runs this project's tests?** `BOARD_AGENT_COMMANDS`,
|
||||
defaulting to what the example ships. This is the one a headless
|
||||
agent cannot work around: a missing test command is a test the work
|
||||
agent may not run.
|
||||
- Every question takes the default on a bare Enter, and the whole
|
||||
sequence is skippable in one keystroke.
|
||||
- The file written is `core/.env.example` with the answered values
|
||||
substituted into their existing lines — comments and all keys intact,
|
||||
so the rest of the settings are discoverable by opening it.
|
||||
- **A non-interactive run never asks and never blocks.** No TTY on stdin
|
||||
(CI, `update.sh`, a hook, `start.sh` under a wrapper) → print one line
|
||||
saying no `.env` exists, defaults apply, and `install.py --setup` will
|
||||
ask — then carry on. `--dry-run` reports what it would ask and writes
|
||||
nothing.
|
||||
- `install.py --setup` re-runs the questions on demand, and is the only
|
||||
way to get them once an `.env` exists: pre-filling from the current
|
||||
file, rewriting it in place. An existing `.env` is otherwise never
|
||||
touched — no repair, no merging in new keys.
|
||||
- `start.sh` needs no logic of its own; it already calls `install.py`
|
||||
before the port dance, which is the right order (setup writes the file,
|
||||
the port branch then edits it).
|
||||
|
||||
**Out of scope**
|
||||
|
||||
- Reconciling an existing `.env` against a newer `.env.example` (new keys
|
||||
after an update). Worth doing, separate card.
|
||||
- Any board-side settings UI, or making settings editable from the page.
|
||||
- Changing what any setting defaults to, or making team mode the default.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- [ ] Given a project with no `manager/local/.env` and a TTY, when
|
||||
`install.py` runs, then it asks the questions above and writes
|
||||
`local/.env` containing every key and comment from
|
||||
`core/.env.example` with the answers substituted.
|
||||
- [ ] Given the same, when every question is answered with a bare Enter,
|
||||
then the written file is behaviourally identical to the shipped
|
||||
defaults — the board behaves exactly as it does today.
|
||||
- [ ] Answering "team" writes both `BOARD_COMMIT_MOVES` and `BOARD_SYNC`
|
||||
on; answering "solo" leaves both empty.
|
||||
- [ ] Given `local/.env` already exists, when `install.py` runs, then it
|
||||
asks nothing, writes nothing and reports "ok" as it does today —
|
||||
including immediately after the setup run (idempotence).
|
||||
- [ ] Given no TTY on stdin, when `install.py` runs with no `.env`, then
|
||||
it prints one line and exits normally without writing or blocking —
|
||||
`update.sh` → `install.py` in a script still completes.
|
||||
- [ ] `install.py --dry-run` with no `.env` writes nothing.
|
||||
- [ ] `install.py --setup` with an existing `.env` asks again, offering
|
||||
the current values as defaults, and rewrites the file.
|
||||
- [ ] Edge case: a first boot in a vendored project both clears the
|
||||
distribution's cards **and** writes `.env` — the first-boot guard
|
||||
still reads false on the next run and the cards stay cleared.
|
||||
- [ ] Edge case: `start.sh` taking a fallback port after setup ran leaves
|
||||
the rest of the file intact and the port line correct.
|
||||
|
||||
## Open questions
|
||||
|
||||
- None.
|
||||
|
||||
## Notes
|
||||
|
||||
Writing the whole of `.env.example` rather than only the answered keys is
|
||||
the deliberate choice: the file becomes the place a project reads to find
|
||||
out what it can change, which is exactly what is missing today. The cost
|
||||
is that it snapshots the example at install time — the drift that the
|
||||
out-of-scope reconciliation card would address.
|
||||
|
||||
**Risks**
|
||||
|
||||
- **Guard ordering.** `first_boot()` returns false as soon as
|
||||
`local/.env` or `local/state/` exists. Setup writing `.env` before
|
||||
`first_boot_clean` runs would permanently skip the clean and leave
|
||||
bench's own cards in the host project. Order is load-bearing, and the
|
||||
edge-case acceptance above is what catches a regression.
|
||||
- **Blocking a script.** `install.py` sits on the path of `start.sh`,
|
||||
`update.sh` and anything automated. A prompt that waits on stdin in
|
||||
those contexts hangs the board start with no output explaining why —
|
||||
hence the TTY check being an acceptance criterion rather than a nicety.
|
||||
- **Turning team mode on is not free.** It needs a reachable `origin`,
|
||||
merge rights for whoever clicks merge, and the discipline that local
|
||||
main only advances through the board. The question should say so in a
|
||||
line; a project that answers "team" by accident gets a board that
|
||||
narrates sync problems it cannot fix.
|
||||
Reference in New Issue
Block a user