One rule for Filbert's R3 and Dewey's B1: y resolves only while the
whole decide box is shown. Adds N9 widths, the start-unguarded and
timeout fixes, and two defects found by the cheap tests (wrap at
graphemes, answer render throw). 51/51 mutants killed; gate on
122b1223 green, cli 110.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
547 lines
28 KiB
Markdown
547 lines
28 KiB
Markdown
# Row 55 (#1544): the full-screen terminal app, candidate packet, rounds 1, 2 and 3
|
||
|
||
Author: Darkwing. Reviewers: Dewey and Filbert. Brief:
|
||
`docs/plans/2026-10-10_design-implementation.md`, section "Full-screen
|
||
terminal app", with `docs/design/IMPLEMENTING.md` "The full-screen
|
||
terminal app" and "Strings to keep exactly". Base: `fe5c68fa` (row 41,
|
||
`base.txt`). It applies unchanged to `23d566d6`: the commits between the
|
||
two touch only BUILD-LOG.md, docs/SESSIONS.md and the queue files, none of
|
||
them in this candidate. The feature is uncommitted; only this packet is
|
||
committed. I pushed nothing.
|
||
|
||
## Files
|
||
|
||
`files.txt` lists the nine repository paths. `candidate-manifest.sha256`
|
||
has their sha256. `build.patch` is `git diff --cached --binary fe5c68fa`
|
||
over them: 9 files, +1363/−16.
|
||
|
||
| File | Change |
|
||
|---|---|
|
||
| `packages/cli/src/app.mjs` | new: key parsing, the `App` state machine, `runApp` (alternate screen, raw mode, resize, SIGTERM, restore) and the quit line |
|
||
| `packages/cli/src/screens.mjs` | new: `draw(state)` is pure and returns rows; Inbox, Tasks, Agents, Trail, Help, overlays, the 16-colour palette and NO_COLOR |
|
||
| `packages/cli/src/cli.mjs` | the decide flow moves into the exported `resolveDecision`, which `decide` and the app share; `SEAT_USAGE`; the no-argument entry `app()` |
|
||
| `packages/cli/src/format.mjs` | `trailSummary` is exported (the Trail screen uses it); no other change |
|
||
| `packages/cli/tests/app.test.mjs` | new: 14 tests, listed below |
|
||
| `packages/cli/README.md` | the intro names row 55; new section "The app" |
|
||
| `docs/TOOLS.md` | the `mosaic` entry: the no-argument line and a four-line paragraph |
|
||
| `scripts/mosaic` | a decided scope addition (Sage, 2026-10-11); see below |
|
||
| `packages/queue/tests/dispatch.test.mjs` | follows from `scripts/mosaic`; in scope (Sage, 2026-10-11); see below |
|
||
|
||
`resolveDecision` writes the same lines in the same order and asks the
|
||
same question as before. `decide` passes `ask = null` for `--yes` and
|
||
keeps its own "stdin is not a terminal" refusal. The 83 tests that were in
|
||
`packages/cli/tests/` at the base still pass unchanged.
|
||
|
||
### Scope addition: scripts/mosaic
|
||
|
||
`scripts/mosaic` is not in the brief's files. Sage approved it on
|
||
2026-10-11 as a decided scope addition. Before, `scripts/mosaic` with no
|
||
argument fell through to `packages/seat/src/cli.mjs`, which printed
|
||
`refused: <seat usage>` and exited 4. The script never reached
|
||
`packages/cli`, so the brief's acceptance line, bare `mosaic` on a TTY
|
||
opens the app, could not be met inside `packages/cli` alone. The change is
|
||
one case that sends no argument to `packages/cli/src/cli.mjs`. Nothing
|
||
else in the script changed.
|
||
|
||
The entry checks in Sage's order:
|
||
|
||
1. Agent markers exit 3 through `refuseInsideAgent`, with the transport's
|
||
message unchanged.
|
||
2. stdin or stdout not a TTY exits 4 with the usage.
|
||
3. Otherwise the app opens.
|
||
|
||
Test 11 below covers each branch through `main([])`. Test 12 spawns
|
||
`scripts/mosaic` itself for exits 4 and 3.
|
||
|
||
### The queue dispatch test
|
||
|
||
My first gate run had test-queue red, 26 passed and 1 failed
|
||
(`out/round1-first-gate/test-queue.log`). `packages/queue/tests/dispatch.test.mjs`
|
||
pins A2's rule that every call but `queue` reaches the seat CLI exactly as
|
||
the pre-A2 script did, and its list of calls included `[]`. Sage's approved
|
||
change sends exactly that call somewhere else, so the pin can't hold as
|
||
written. The file is outside the brief. I changed it because the gate
|
||
needs every suite green and the change follows from the approved one.
|
||
Sage ruled it in scope on 2026-10-11, after the review request: a decided
|
||
scope addition, with the candidate unchanged.
|
||
|
||
The change, 16 lines in and 5 out:
|
||
|
||
- `[]` leaves the list of seat calls. `[""]` stays: it is one argument, so
|
||
it still goes to the seat CLI.
|
||
- The fake root gains a fake `packages/cli/src/cli.mjs`.
|
||
- A new test: no argument reaches `packages/cli/src/cli.mjs` with empty
|
||
argv, the same working directory and stdin, and the fake's exit code
|
||
and output passed through. The same call on the pre-A2 fixture still
|
||
reaches the seat CLI.
|
||
- The header comment names row 55.
|
||
|
||
With `scripts/mosaic` put back to the base, the new test fails and the
|
||
other three pass. With the candidate, all four pass. The test that A2 only
|
||
adds lines to the pre-A2 script still passes, because the row 55 case adds
|
||
four lines and removes none.
|
||
|
||
## How the brief maps to the candidate
|
||
|
||
| Brief | Where | Test |
|
||
|---|---|---|
|
||
| no argument on a TTY opens the app | `cli.mjs` `app()`, `app.mjs` `runApp` | 11, 12; `out/smoke.txt` |
|
||
| not a TTY: usage, exit 4 | `cli.mjs` `app()` | 11, 12 |
|
||
| agent run: exit 3 | `refuseInsideAgent` in `transport.mjs` | 11, 12 |
|
||
| Inbox, Tasks, Agents, Trail, Help; keys c i t a r j k : ? q | `app.mjs` `#key`, `screens.mjs` | 1, 9 |
|
||
| decide runs the existing flow, strings unchanged | `resolveDecision` | 4, 5, 6, 7, 13 |
|
||
| 16 colours, NO_COLOR, 80x24, designed at 120x36 | `screens.mjs` | 1, 3 |
|
||
| no new dependency | `package.json` untouched | |
|
||
| every screen fits 120 columns and 80x24 | | 1 |
|
||
| NO_COLOR output has no escape codes | | 3 (see deviation 1) |
|
||
| the string grep test | | 13 |
|
||
|
||
The tests in `packages/cli/tests/app.test.mjs`:
|
||
|
||
1. Every screen and overlay fits 120x36, 80x24 and the narrow layout
|
||
(100x30, 99x24), row for row, in colour and NO_COLOR. That covers every
|
||
view and overlay, plus the empty, stale, refused and no-business states.
|
||
2. Broker text is shown, never interpreted. No raw control character or
|
||
bidi override reaches the terminal (`visible()` from
|
||
`packages/conversation/src/terminal.mjs`).
|
||
3. NO_COLOR: no escape codes except reverse video (7/27) on the selected
|
||
row. A frame with nothing selected has none.
|
||
4. 1-9 shows the decide flow's own lines and question. Only y resolves,
|
||
and it goes through the test broker.
|
||
5. The app's decide text equals `mosaic decide` stdout for the same
|
||
decision.
|
||
6. n, N, Esc, Enter and q at the question cancel. Other keys wait.
|
||
Nothing is sent.
|
||
7. Outcome unknown shows the CLI's sentence and offers no retry. Then v
|
||
opens the trail, or r reads again.
|
||
8. Refused and failed reads keep the last list, marked stale. A refused
|
||
decide says refused.
|
||
9. Keys: views, moving, the trail of a selection, the command line, help
|
||
and quit.
|
||
10. No business: the app says so, and `:business <id>` picks one.
|
||
11. `main([])`: exit 3 with a marker set, exit 4 with the usage off a
|
||
terminal, and the app on one (stubbed `runApp`).
|
||
12. `scripts/mosaic` with no argument reaches the app entry (exit 4 off a
|
||
terminal, 3 with a marker). `SEAT_USAGE` still equals what the seat
|
||
CLI prints, so the copy can't drift.
|
||
13. Strings to keep exactly appear once, in `cli.mjs` and `transport.mjs`.
|
||
`app.mjs` and `screens.mjs` don't restate "authorizes the action",
|
||
"declines the action", "[y/N]", "may have recorded it" or "runs only
|
||
from a human shell".
|
||
14. `draw` is pure: the same state gives the same frame.
|
||
|
||
## Deviations, stated
|
||
|
||
1. **NO_COLOR keeps reverse video on the selected row.** The brief's test
|
||
line says NO_COLOR output has no escape codes. `IMPLEMENTING.md` line
|
||
188 says reverse video stays for the selected row, and
|
||
`docs/design/README.md` line 148 makes reverse video the selection
|
||
mark. I followed the design. Test 3 checks that 7 and 27 are the only
|
||
SGR codes, and that a frame with no selection has none. The writer's
|
||
`\x1b[H\x1b[2J` before each frame and the alternate screen codes are
|
||
cursor and screen control, not colour.
|
||
2. **The non-TTY message gains one line.** It prints
|
||
`mosaic: the app needs a terminal on stdin and stdout; from a script,
|
||
use a verb:` and then the seat usage and the `packages/cli` usage, exit
|
||
4. Before, it printed `refused: <seat usage>`. `IMPLEMENTING.md` line
|
||
191 says to refuse and point at the verbs, and the old text named only
|
||
the seat verbs. If a reviewer wants the old bytes, test 12 already
|
||
holds them as `SEAT_USAGE`.
|
||
3. **The Terminal class is not the base.** The brief says the app is built
|
||
on the conversation `Terminal` class. Only `visible()` is imported.
|
||
`Terminal` is a line editor for Talk, and Talk is out of scope here. The
|
||
app needs a whole-screen redraw and single-key input, which `Terminal`
|
||
doesn't do. When Talk joins, the Talk screen embeds `Terminal`.
|
||
4. **Reads block redraw.** `humanTransport` uses `spawnSync`, so the
|
||
screen doesn't redraw during a read (30 s timeout). That is the existing
|
||
transport, unchanged. The README says so.
|
||
|
||
## Evidence, round 1
|
||
|
||
All runs in a detached worktree of `23d566d6` with `build.patch` applied
|
||
and `node_modules` linked (`out/gate/`).
|
||
|
||
- `packages/cli`: 97/97 (83 before, plus the 14 new), `out/gate/cli-tests.log`.
|
||
- Every `scripts/test-*.sh`, run in sequence on the final candidate, all
|
||
exit 0 (`out/gate/summary.txt`, logs beside it):
|
||
|
||
| Suite | Result |
|
||
|---|---|
|
||
| test-auth | 15 passed, 0 failed |
|
||
| test-conductor | 17 passed, 0 failed |
|
||
| test-config | 24 passed, 0 failed |
|
||
| test-discord | 66 passed, 0 failed |
|
||
| test-extension-package | 18 passed, 0 failed |
|
||
| test-foundation | 44 passed, 0 failed |
|
||
| test-queue | 27 passed, 0 failed; canonical-root checks skip in a worktree |
|
||
| test-release | 14 passed, 0 failed |
|
||
| test-task | 26 passed, 0 failed; 4 Docker groups skipped |
|
||
|
||
test-task ran with `DOCKER_HOST` pointed at a missing socket, so its
|
||
adapter, workspace, live task and live recall groups printed skip. The
|
||
live ones call a real model. Nothing this candidate changes runs in
|
||
those groups. Sage's gate rerun covers them with Docker.
|
||
- The first gate run, before the dispatch test change, is kept in
|
||
`out/round1-first-gate/`. `out/dispatch-mutant.txt` has the dispatch
|
||
test against the base script and the candidate script.
|
||
- Smoke, `smoke.sh`: the app in a pty at 80x24 against a fixture config,
|
||
never the real bus. Keys ? Esc t q. The alternate screen was entered once
|
||
and left once, then it printed `(app closed with q; 0 decisions resolved
|
||
this session)` and exit 0 (`out/smoke.txt`).
|
||
- `out/apply.txt`: `build.patch` applies to `23d566d6` in a fresh
|
||
worktree and the manifest matches.
|
||
|
||
## Follow-ups (not in this row)
|
||
|
||
- The transport's "did not finish" `CliError` (`transport.mjs` line 60)
|
||
has no `.code`, so the outcome-unknown branch in `resolveDecision` does
|
||
not see it. `mosaic decide` then prints
|
||
`outcome-unknown: the bus transport did not finish`, exit 1, instead of
|
||
the "may have recorded it" sentence. The app matches the message text
|
||
and shows its outcome-unknown overlay, so only the verb has the gap. It
|
||
predates this row. Written up in `FOLLOWUP-transport-code.md`; Sage
|
||
queues it with the 2026-10-12 batch.
|
||
- Talk and stop in the app, Decision Seen, and the web console stay out of
|
||
scope, as the brief says.
|
||
|
||
|
||
## Round 2
|
||
|
||
Round 1 drew changes requested from both reviewers: Filbert in comment
|
||
27251 (rev 390) and Dewey in comment 27253 (rev 395), both on candidate
|
||
`93fa48c1cdfe`. This round answers every blocking item and each named
|
||
mutant, and most of the non-blocking ones. `build.patch`, `files.txt`
|
||
and `candidate-manifest.sha256` now hold the round 2 candidate. The
|
||
round 1 versions are in this packet's history at `4acadaae`. The same
|
||
nine files: +1741/−16. Base for the apply check: `c506c6ec`
|
||
(`out/gate-round2/apply.txt`). None of the commits since `fe5c68fa` touch the nine
|
||
files.
|
||
|
||
### Blocking
|
||
|
||
**R1 (Filbert): the question took type-ahead.** Now y or Y resolves and
|
||
any other key answers No. Quitting at the question (q, Ctrl-C, a signal,
|
||
end of input) also answers No and sends nothing. Keys that were queued when
|
||
the question opened, or that arrive within `ASK_MS` (200 ms) of it being
|
||
drawn, are dropped. The timer arms the question; arming empties the key
|
||
queue. That covers a paste (`1 yes`), `decide 1a2b yes` typed without the
|
||
colon, and keys typed while a `spawnSync` read held the screen: those
|
||
arrive as data events after the draw, inside the window. A timer refresh
|
||
tick is never taken as an answer. The hint now reads "y resolves · any
|
||
other key cancels (the default is No)".
|
||
|
||
200 ms is my choice. It is short enough that a person who reads the
|
||
question first never meets it, and long enough to swallow keys the
|
||
terminal already holds. A y pressed inside it is dropped silently; the
|
||
next one counts. Tests pass `askMs: 0`, except the window test, which
|
||
uses 300 ms and sends one y at once and one 10 ms after the draw.
|
||
|
||
**R2 (Filbert): `cells()` miscounted emoji.** `\p{Emoji_Presentation}`
|
||
counts 2, and U+FE0F after a one-cell `\p{Emoji}` base adds the second
|
||
cell (🅰️, ❤️, 1️⃣). The old range 1F300-1FAFF became Emoji_Presentation
|
||
plus 1F200-1F2FF. `width`, `fit` and `wrap` pass the previous code point.
|
||
The fake inbox now carries ✅✅, ⚡ and 🅰️; a task title carries 発行 ✅.
|
||
The fits test covers them at every size, and a width test checks known
|
||
widths plus the UTF-16 length of those rows on screen, which does not
|
||
depend on `width()`. Known limit: ZWJ sequences and flags count more cells
|
||
than they draw, so such a row comes out short. It can't overflow.
|
||
|
||
**B1 (Dewey): a tall decide box cut the authorization lines.** The box now
|
||
cuts inside the question. The decision line, the class line, the options,
|
||
the recommendation, `action: A on T`, the authorizes-it line, the task and
|
||
everything from `your choice:` on always stay. A box still too tall
|
||
(a small screen) fall back to the old cut, which keeps the top and the
|
||
part from `your choice:` on, then the bottom. The cut line now says the
|
||
whole text is in `mosaic inbox --json`; `formatInbox` caps the question at
|
||
400 characters, so plain `mosaic inbox` was wrong. The new test raises a
|
||
question of more than 530 characters with a target and a task at 80x24 and
|
||
checks that each of those lines, the cut line and `[y/N]` are on screen,
|
||
and that the end of the question is what went.
|
||
|
||
### Filbert's mutants
|
||
|
||
`out/mutants-round2/` has the runner and its output: 30 mutants, each one
|
||
edit to `src/`, with the whole `packages/cli` suite run against it. All 30
|
||
are killed. `no-arm-delay` (the window set to 0) survived the first run,
|
||
because the in-window y in the test arrived synchronously. The test now
|
||
also sends one 10 ms later, and the rerun kills it.
|
||
|
||
| Mutant | Killed by |
|
||
|---|---|
|
||
| `resolve-ask-skipped-when-note` | decide with --note still asks |
|
||
| `unknown-space-only` | outcome unknown: the hyphenated transport error with no code |
|
||
| `restore-no-altleave`, `restore-no-raw-off`, `no-sigterm` | runApp leaves the terminal as it found it |
|
||
| `paint-no-reverse` | NO_COLOR: exactly one reverse-video row, in colour and NO_COLOR |
|
||
| `cells-no-cjk` | width counts terminal cells (the 発行 ✅ row) |
|
||
| `overlay-cut-loses-prompt` | every screen fits (the long decide shot keeps the prompt) |
|
||
| `app-host-not-live` | the app's business comes from a live bus host only |
|
||
| `quit-no-answer` | quitting at the question answers No, armed or not |
|
||
| `refresh-in-overlay` | a timer tick reads nothing under an overlay |
|
||
| `unknown-esc-stays` | outcome unknown: Esc closes the box |
|
||
| `trail-task-id` | keys: v on Tasks opens the trail of `vikunja:1/7` |
|
||
| `business-keeps-data` | keys: `:business other` drops lists and selections |
|
||
| `nav-no-brackets` | every screen fits: `[i Inbox 3]` wide, `[i 3]` narrow |
|
||
|
||
The runner adds mutants for this round's own changes: no SIGINT, no SIGHUP,
|
||
an unguarded resize, no Emoji_Presentation, no FE0F rule, the B1 cut
|
||
removed, type-ahead kept, no arm delay, other keys waiting, no capital Y,
|
||
a tick taken as an answer, a tick dropping the resolved status, the
|
||
command line typing key names, "Press 1-1" and no too-small guard.
|
||
|
||
### Non-blocking
|
||
|
||
- **N1 (Filbert; Dewey concurs).** The command line takes only one-character
|
||
keys, so the arrows type nothing. Tested.
|
||
- **N2.** `runApp` listens for SIGTERM, SIGINT and SIGHUP, and a resize or
|
||
a first draw that throws goes to `crash`, so the restore runs. Tested with fakes
|
||
for each, plus q and end of input: the first write enters the alternate
|
||
screen, the last leaves it, raw mode goes on then off, and every listener
|
||
is gone.
|
||
- **N3.** Below 40x10 the frame says the terminal is too small and how big
|
||
it must be, and still fits row for row. Tested at 39x10, 40x9, 3x3 and
|
||
1x1; 40x10 joins the fits sizes.
|
||
- **N4.** A timer tick keeps a "resolved" status. Tested.
|
||
- **N5.** An inbox read that times out inside the decide flow shows the
|
||
outcome-unknown box though nothing was sent. Not changed here. It is
|
||
added to `FOLLOWUP-transport-code.md`, because the fix belongs with the
|
||
transport's missing `.code`.
|
||
- **N6.** One option says "Press 1 to choose". Tested.
|
||
- **N7.** `runApp` has a test (N2 above).
|
||
- **N8.** The dispatch test now also checks that the no-argument call gets
|
||
the same environment as it did under the pre-A2 script. The old non-TTY
|
||
bytes: deviation 2 stands. If Sage wants them back, it is one line in
|
||
`cli.mjs`, and test 12 already holds the bytes as `SEAT_USAGE`.
|
||
- **Dewey: Ctrl-C on the command line.** It closes the command line; it
|
||
does not quit. The `app.mjs` header and the README now say so.
|
||
- **Dewey: REACHES YOU.** Listed below as deviation 5.
|
||
|
||
### Deviation 5
|
||
|
||
5. **The Inbox has no REACHES YOU column.** In the design it says when
|
||
the decision reaches you: `now`, or a digest time such as `08:00 CT`.
|
||
The broker's inbox rows say nothing about delivery: no notifier time,
|
||
no digest hour. The app could guess from the class, but a wrong time
|
||
in that column would mislead, so I left it out. When the inbox rows
|
||
carry delivery, the column is one more entry in `inboxBody`.
|
||
|
||
### Tests, round 2
|
||
|
||
`packages/cli/tests/app.test.mjs` has 23 tests (14 in round 1). New:
|
||
width counts terminal cells; type-ahead never answers the question;
|
||
quitting at the question; a timer tick under an overlay; the B1 long
|
||
question; one option says Press 1; decide with --note still asks; the
|
||
business comes from a live host only; runApp restores the terminal.
|
||
Changed: test 6 is now "any key but y or Y answers No" (n N Esc Enter q x
|
||
space 2 Ctrl-C and the up arrow; none sends, none quits), and test 8
|
||
sends 1 and y as separate inputs. Tests 1, 3, 4, 7 and 9 gained the
|
||
assertions named above.
|
||
|
||
### Evidence, round 2
|
||
|
||
|
||
All runs in a detached worktree of `c506c6ec` with the round 2
|
||
`build.patch` applied and `node_modules` linked. `out/gate-round2/` has
|
||
the logs, `summary.txt` and `gate.sh`, the script that ran them in
|
||
sequence.
|
||
|
||
- `packages/cli`: 106/106, `out/gate-round2/cli-tests.log`.
|
||
- Every `scripts/test-*.sh`, all exit 0:
|
||
|
||
| Suite | Result |
|
||
|---|---|
|
||
| test-auth | 15 passed, 0 failed |
|
||
| test-conductor | 17 passed, 0 failed |
|
||
| test-config | 24 passed, 0 failed |
|
||
| test-discord | 66 passed, 0 failed |
|
||
| test-extension-package | 18 passed, 0 failed |
|
||
| test-foundation | 44 passed, 0 failed |
|
||
| test-queue | 27 passed, 0 failed; canonical-root checks skip in a worktree |
|
||
| test-release | 14 passed, 0 failed |
|
||
| test-task | 26 passed, 0 failed; Docker groups skipped, as in round 1 |
|
||
|
||
A `gate_default` Docker network appeared during the run. It had no
|
||
containers afterwards, and I removed it.
|
||
- `out/gate-round2/apply.txt`: `build.patch` applies to `c506c6ec` and
|
||
the manifest matches. The nine files are byte-identical at `fe5c68fa`
|
||
and `c506c6ec`.
|
||
- `out/gate-round2/smoke.txt`: `smoke.sh` at 80x24 against a fixture
|
||
config, never the real bus. The alternate screen was entered once and
|
||
left once, the quit line printed, exit 0.
|
||
- `out/mutants-round2/`: `run2.mjs` and its output. `round2.txt` is the
|
||
run of all 30 mutants: 29 killed, `no-arm-delay` survived. I fixed the
|
||
type-ahead test (a second y 10 ms after the draw), then
|
||
`round2-rerun.txt` reran the two type-ahead mutants: both killed. The
|
||
other 28 were not rerun; the fix only adds a send and an assertion to
|
||
one test.
|
||
- Candidate manifest sha256:
|
||
`3f2d8f8ea3fea1cf7a002297ee9b23059f1522e8060c1d981fba19c85baeabd7`.
|
||
|
||
## Round 3
|
||
|
||
Round 2 drew changes requested from both reviewers on candidate
|
||
`3f2d8f8e`. Filbert's comment 27269 (rev 403) had one blocking item, R3,
|
||
plus N9 and three cheap tests. Dewey's comment 27278 (rev 407) had one
|
||
blocking item, B1 again, plus two non-blocking ones. Dewey asked for one
|
||
rule covering B1 and R3, and this round has one. `build.patch`,
|
||
`files.txt` and `candidate-manifest.sha256` now hold the round 3
|
||
candidate. The round 2 versions are in this packet's history at
|
||
`fa6d889c`. The same nine files: 9 files, +1932/−16 against `fe5c68fa`. Base for the apply check:
|
||
`122b1223` (`out/gate-round3/apply.txt`). None of the nine files changed
|
||
between `fe5c68fa` and `122b1223`.
|
||
|
||
### Blocking: R3 and B1, one rule
|
||
|
||
**The defects.** R3 (Filbert): below 40x10 the frame shows only the
|
||
too-small text, and `y` still resolved the question it hid. B1 (Dewey):
|
||
between 40x10 and about 80x21 the decide box didn't fit, and `draw()`'s
|
||
general fallback cut it from the top and the bottom. The action, what
|
||
the choice authorizes, the recommendation and the task were dropped,
|
||
and at 80x10 and 40x12 "your choice:" too, while `y` still resolved.
|
||
|
||
**The rule.** `y` or `Y` resolves only while the frame shows the whole
|
||
decide box, with the end of a long question as the one thing it may
|
||
cut. Every other case answers No and sends nothing.
|
||
|
||
- `decideBox(o, w, room)` in `screens.mjs` builds the box. If it is
|
||
taller than the body, it cuts the end of the question and keeps the
|
||
rest: the decision line, the options, the recommendation, the action
|
||
and target, what the choice authorizes, the task, "your choice:", the
|
||
question and its keys. If those don't fit either, it returns null.
|
||
- `decideShown(o, cols, rows)` is true when the terminal is at least
|
||
40x10 and `decideBox` returns a box. It is exported and `App#ask`
|
||
uses it: `answer((k === "y" || k === "Y") && decideShown(...))`.
|
||
- `App#ask` reads the size at each key, not when the question was
|
||
armed. A terminal shrunk after the question appeared answers No, and
|
||
one grown back first lets `y` resolve.
|
||
- When `decideBox` returns null, `overlayBox` draws a small box: "Too
|
||
small to show this decision." and "Any key answers No." (or
|
||
"resolving…" when the question isn't open yet).
|
||
- Below 40x10 the too-small text ends "A decision question is open; any
|
||
key answers No." while the question is open, instead of "q quits",
|
||
since q answers No there.
|
||
- `draw()`'s general fallback now applies only to help and to a long
|
||
detail. A decide box always fits by the time it reaches `draw()`. The
|
||
fallback no longer slices: the body is at least 5 rows, so top, cut
|
||
line and last two lines are exactly that many.
|
||
|
||
The overlayBox comment and the README bullet said the must-keep lines
|
||
were never lost. Both now describe the rule and the hidden box. The
|
||
README states the sizes I measured for a gated decision with a task:
|
||
20 rows at 80 columns, 21 with a cut question, and 29 rows at 40
|
||
columns. `out/probe-round3.mjs` measured them, and its output is
|
||
`out/probe-round3.txt`. The smallest row count per width, short question
|
||
40:29, 50:26, 60:22, 80:20, 120:20; long question 40:30, 50:27, 60:23,
|
||
80:21, 120:21.
|
||
|
||
**Tests.**
|
||
|
||
- "below 40x10 the question is hidden, so every key answers it No, y
|
||
included (Filbert R3)": at 30x24 and 80x9 from the start, and at 80x24
|
||
shrunk to 30x24 after arming, `y`/`Y` sends nothing, the status is
|
||
`not resolved` and the decision stays in the Inbox. The frame shows
|
||
the new text and not "q quits". Shrunk and grown back before the key,
|
||
`y` resolves.
|
||
- "a decide box too big for the terminal says so, and every key answers
|
||
it No, y included (Dewey B1, round 3)": a gated decision with a task.
|
||
Dewey's sizes, short question at 80x18 and 40x24 and long question at
|
||
80x20, plus 120x16 and 80x10: the frame says "Too small to show this
|
||
decision" and `y` sends nothing. At 80x20 short and 80x21 long, the
|
||
smallest sizes that show it, every must-keep line is on screen and `y`
|
||
resolves. Armed at 80x24 and shrunk to 80x15 before the key, `Y`
|
||
answers No.
|
||
- "at every size the decide box shows each line y depends on, or says
|
||
it is hidden": columns 40 to 120 in steps of 4, rows 10 to 40, short
|
||
and long questions. At each size either `decideShown` is true and the
|
||
frame has all ten must-keep lines, or it is false and the frame has
|
||
the hidden box. Each side holds more than 100 sizes.
|
||
- The "every screen and overlay fits" test now expects the hidden box
|
||
for its long overlay at the narrow layout, not a cut box.
|
||
|
||
### Non-blocking
|
||
|
||
**Dewey: `start-unguarded` survived.** The runApp test now has a case
|
||
where the terminal's write throws on the first frame. runApp must
|
||
reject with that error and leave the terminal as it found it. The
|
||
`.then` handler is attached at the call, because the first frame's
|
||
error rejects before the next tick.
|
||
|
||
**Dewey: four mutants were killed only by a hang.** `{ timeout: 10000 }`
|
||
is now on "decide with --note still asks", "quitting at the question",
|
||
and the runApp test, which covers `no-sigterm` and `no-sighup`.
|
||
|
||
**N9 (Filbert).** `cells()` now counts the remaining East Asian Width W
|
||
and F ranges as 2: U+2329, U+232A, U+A960-A97C, U+FE10-FE19,
|
||
U+FE50-FE6B, U+16FE0-16FF1, U+17000-18D08, U+1AFF0-1B2FB. The width
|
||
test checks one character from each, plus U+1AFF0's range end, U+1B2FB.
|
||
|
||
**Cheap tests (Filbert).**
|
||
|
||
- U+1F210 measures 2 (width test).
|
||
- A throw from `answer`. The test makes the terminal write throw after the
|
||
question is drawn, then presses `y`. This found a defect. `answer` threw
|
||
from its `render()` before calling `resolve`, so the decide job never
|
||
ended: the pump crashed the app, but `this.job` stayed pending.
|
||
`answer` now resolves No when its render throws, then rethrows to the
|
||
pump. The test checks the quit is `an error` with that error, the job
|
||
ends, and nothing is sent.
|
||
- ❤️ at a wrap edge. This also found a defect. `wrap()` broke long words
|
||
between code points, so `wrap("abc❤️ x", 4)` gave `abc❤` and a line
|
||
starting with a lone U+FE0F. It now breaks between graphemes
|
||
(`Intl.Segmenter`), so the result is `abc` and `❤️ x`. The test also
|
||
checks a line of marks and ❤️ never comes out wider than asked.
|
||
|
||
The "runLoop never overlaps ticks" flake is in the notifier tests, not
|
||
in this candidate. I left it to the notifier's owner.
|
||
|
||
### Tests, round 3
|
||
|
||
`packages/cli/tests/app.test.mjs` has 27 tests (23 in round 2); the cli
|
||
suite has 110. New: the R3 test, the answer-throw test and the two B1
|
||
tests. The width test gained the N9 characters, U+1F210 and the wrap
|
||
checks. The runApp test gained the first-frame error case.
|
||
|
||
### Evidence, round 3
|
||
|
||
All runs were in a detached worktree of `122b1223` with the round 3
|
||
`build.patch` applied and `node_modules` linked. `out/gate-round3/` has
|
||
the logs, `summary.txt` and `gate.sh`, the script that ran them in
|
||
sequence.
|
||
|
||
- `packages/cli`: 110/110, `out/gate-round3/cli-tests.log`.
|
||
- Every `scripts/test-*.sh` exited 0:
|
||
|
||
| Suite | Result |
|
||
|---|---|
|
||
| test-auth | 15 passed, 0 failed |
|
||
| test-conductor | 17 passed, 0 failed |
|
||
| test-config | 24 passed, 0 failed |
|
||
| test-discord | 66 passed, 0 failed |
|
||
| test-extension-package | 18 passed, 0 failed |
|
||
| test-foundation | 44 passed, 0 failed |
|
||
| test-queue | 27 passed, 0 failed; canonical-root checks skip in a worktree |
|
||
| test-release | 14 passed, 0 failed |
|
||
| test-task | 26 passed, 0 failed; Docker groups skipped, as in rounds 1 and 2 |
|
||
|
||
A `gate_default` Docker network appeared during the run. It had no
|
||
containers afterwards, and I removed it.
|
||
- `out/gate-round3/apply.txt`: `build.patch` applies to `122b1223` and
|
||
the manifest matches.
|
||
- `out/gate-round3/smoke.txt`: `smoke.sh` at 80x24 against a fixture
|
||
config, never the real bus. The alternate screen was entered once and
|
||
left once, the quit line printed, and the exit code was 0.
|
||
- `out/probe-round3.mjs` and `out/probe-round3.txt`: the size probe the
|
||
README numbers come from. Run it with `CLI=<checkout>/packages/cli node
|
||
--test out/probe-round3.mjs`.
|
||
|
||
- `out/mutants-round3/`: `run3.mjs` and its output `round3.txt`, 51
|
||
mutants. Round 2's 30 less `overlay-cut-loses-prompt`, which edited a
|
||
line this round removed. Four re-anchored to lines this round changed
|
||
(`other-key-waits`, `no-capital-y`, `tiny-no-guard`,
|
||
`b1-cut-keeps-end`). Then 22 for round 3, among them
|
||
`r3-y-ignores-shown`, `r3-shown-is-min-size` (y checks only 40x10),
|
||
`r3-shown-at-arm` (the size read once, when armed),
|
||
`r3-no-question-whole-box` and `r3-start-unguarded`. All 51 were
|
||
killed, and the three source files matched the candidate afterwards.
|
||
One run, no fix needed.
|
||
- Candidate manifest sha256: `59f1b9b4dabfb47dc79641de209317cfd538cb27bd484b5391a0703a8c84c60b`.
|