%% GROUP R Recorded Recorded from your messages
Your decisions of 2026-11-02 in chat. R3 is done, in its second form (the table at the top uses it); R1 and R2 wait for the work they belong to.

%% GROUP Y Yardsticks Yardsticks and corpus
How the corpus figures are computed. Y1 (rewritten on the evening of 2026-11-02) now sorts the shells' differences without our parser; Y2 was rewritten from your note; Y3 cleans the corpus.

%% GROUP P Parser Parser additions
P1–P4 are done in 0.03 (your ruling in chat: what both shells run alike must be accepted). P5 settles `[` and `test`. P6 (new) lists more forms that both shells run alike, found by the new yardsticks.

%% GROUP Q Questions Questions for you
New on the evening of 2026-11-02. Q1 and Q2: two of the rules conflict, and the order of the rules (`doc/large-subset/large-subset-state.md`, section 2) needs your choice. Q3: four verdicts that no probe and no manual settles.

%% GROUP B Builtins Builtins left to sh
Your idea: model `alias` like functions (B1), and let the interactive builtins do nothing, by a caller's choice (B2); B3 runs the non-interactive ones as dash does. These need both the parser and the executor. Together they are the largest group of rejections within the lower yardstick.

@@ R1 | Recorded from your messages | test | Leave `[ a \< b ]` and `[ b \> a ]` to the caller, by an option
## now
Your message: you do not feel qualified to decide `<` and `>` in `test`, so it goes to the user of the module.

The facts: `[ a \< b ]` compares strings. dash and bash both answer 0 (true). The program `/usr/bin/[` of coreutils, which the subset runs for `[` and `test`, does not know `<` and `>`: it prints `[: '<': binary operator expected` and returns 2.

## plan
A parser option, off by default (as the existing option `fdmulti` is, which allows descriptor numbers of two or more digits). Off: `ShellSubsetLarge` rejects `[` or `test` with a literal `<` or `>` argument, so `/bin/sh -c` runs the string. On: it accepts them, and the program `[` runs them (and fails, as above); this is for a caller who wants coreutils' behaviour everywhere (P5). Written together with P5.

## nogo
Not applicable: this is your decision. Write in a note if I misread it.

## refs
`ShellSubsetLarge.pm`; large spec, section 3 (and a line in the list of options).

@@ R2 | Recorded from your messages | names | Rename the large parser to the new `Proc::` names when this round of work ends
## now
Your message: the posix_spawn session renamed its modules under `Proc::` (`Proc::ShellSpawn`, `Proc::ShellSubset::Small`, …), and when I finish working on my unrenamed files I must synchronize with them and take the same names.

## plan
At the end of this round: take the posix_spawn session's newest bundle, follow its rename table (in its `docs/pspawn-spawner.md`), rename `ShellSubsetLarge.pm` to the matching name (presumably `Proc::ShellSubset::Large`), and update the tests, the measurement scripts and the docs. Also recorded in my handoff file, so a later session does it if I cannot.

## nogo
Not applicable: this is your decision.

## refs
All `large-subset-*` files; `ShellSubsetLarge.pm`.

@@ R3 | Recorded from your messages | yardsticks | Measure against two yardsticks decided by the shells, not by our parser: upper ("nothing bogus found") and lower ("compatible")
## now
Your messages of 2026-11-02:
- A difference between dash and bash is **blatant** when "either one of them fails or does not match the docs; or they behave in blatantly different ways".
- "Other mismatches in how dash/bash behave are subtle."
- The yardsticks measure features of the shells, never our parser.
- Where both shells refuse a form, we refuse it too.
- For the commands the subset runs as programs (`echo`, `printf`, `test`, `[`, `kill`, `time`; also `pwd`, `true`, `false`, where the shells agree anyway), the program beats the shells' difference.

## plan
Done, in a second version that replaces the first (which sorted our parser's messages; see Y1). A sample is within the **upper yardstick** when `dash -n` and `bash --posix -n` both accept it, and it uses no blatant difference. It is within the **lower yardstick** when, in addition, it uses no subtle difference. The differences are found in the sample's text and in ShellCheck's warnings about constructs dash lacks. Each comes with a verdict and with who decided it (Y1). Our parser plays no part. The figures are in the table at the top of this page; the full account is in `doc/large-subset/large-subset-state.md`.

## nogo
This records your decision. Go: the record above is right. No-go: say in a note what is wrong.

## refs
`doc/large-subset/large-subset-state.md`; `doc/large-subset/tools/large-subset-yardsticks.pl`; `doc/large-subset/tools/large-subset-features.pl`.

@@ Y1 | Yardsticks and corpus | judgement | Keep the new sorting of dash/bash differences, made from the shells and bash's manual, not from our parser
## now
The first version of this item sorted our parser's rejection messages into blatant and subtle, so the yardsticks measured the parser against itself. It is withdrawn. Now every difference between the shells is a feature with a verdict and a basis, that is, who or what decided it:
- a probe run in both shells (148 probes);
- the bash manual's section "Bash POSIX Mode" (bash 5.2.21). For example, its item 38 says that `kill` refuses signal names with a `SIG` prefix, so `kill -SIGHUP` failing in bash is as documented;
- ShellCheck's statement that dash lacks a construct (so dash fails);
- or, in four cases, Claude's judgement, which stays open until you decide (Q3).

Found on the way: bash 5.2.21 does not match its manual's item 24, "If a POSIX special builtin returns an error status, a non-interactive shell exits". Example: `bash --posix -c 'shift 3; echo after'` prints `after`. By your definition this is blatant. It is recorded as known issue X22 in `doc/pspawn-known-issues.md`.

## plan
Keep this sorting. Any verdict can be changed in `doc/large-subset/tools/large-subset-verdicts.txt`, also as free-form text when a decision is postponed. The figures are then recomputed in seconds, without a rescan. Tell me in a note which verdict to change.

## nogo
Tell me in a note what should decide the verdicts instead.

## refs
`doc/large-subset/large-subset-dialect-features.md` (every feature with its verdict and basis); `doc/large-subset/tools/large-subset-verdicts.txt`.

@@ Y2 | Yardsticks and corpus | NL2Bash | Accept a final backslash as a literal `\`, as both shells do
## now
NL2Bash (a collection of one-line commands taken from scripts) has 15 commands that end in a backslash. The original command went on in the next line, and the collection cut it there. Example: `find . -inum $inum -exec rm {} \`. Both shells read a backslash at the very end as a literal `\`: `echo a\` prints `a\` in both. Our parser rejects these strings with the message `final backslash`.

## plan
Rewritten from your note on this item ("If they match, we have no choice"). Accept a final backslash as a literal `\`, as both shells do, and keep these samples in the measurement. Tests, checked in both shells.

## nogo
They stay rejected, and stay counted as rejected within the lower yardstick.

## refs
`Proc::ShellSubset::Large`; `t/parser/large-subset-tests.pl`.

@@ Y3 | Yardsticks and corpus | tldr | Turn tldr's `<Enter>` in here-documents into newlines, and leave out other lines with keystrokes
## now
tldr (a collection of short help pages) writes some multi-line examples on one line, with `<Enter>` where the reader presses Enter. Example: `cat <<EOF >path/to/file.txt <Enter> multiline_data <Enter> EOF` means three lines: `cat <<EOF >path/to/file.txt`, then `multiline_data`, then `EOF`. Our parser looks for the here-document's body on the next line, finds none, and rejects the string with `here-document body not found` (3 samples).

Most of tldr's 42 lines with `<Enter>` are not shell at all but keystrokes inside a program, for example `%s/regex/replacement/g<Enter>` in vim. The key-notation rule of the extraction catches only lines made of keys alone, so these stay in and count as rejected.

## plan
In the extraction: in a line that has a here-document (`<<`), replace ` <Enter> ` by a newline (the original line kept in the sample's field `orig`); leave out every other line that contains a keystroke such as `<Enter>`, `<Esc>`, `<Tab>`, `<Ctrl …>` next to other text, with the reason recorded in `drops.tsv`.

## nogo
These lines stay as they are, counted as rejected.

## refs
`large-subset-extract.pl`; `large-subset-drops.pl`; `claude/large-subset-ceiling.md`.

@@ P1 | Parser additions | braces | Accept literal braces inside words
## now
A `{` or `}` inside a word is literal text in both shells unless it forms a brace expansion (bash only; with a comma or `..` between the braces). Example: `echo @{-1} x{a}y {} {.}.png \{}` prints `@{-1} x{a}y {} {.}.png {}` in dash and in `bash --posix`. Our parser rejects it: `unquoted '{'` (26 samples within the lower yardstick, e.g. `git check-ref-format --branch @{-1}`, `parallel convert {} {.}.png ::: *.jpg`, `find … -exec cmd \{} \;`).

## plan
**Done in 0.03** (your ruling in chat). Accept `{` and `}` inside a word, and as a word of their own after the command name, when no unquoted `,` and no `..` lies between a `{` and its `}`. `{` and `}` as the first word of a command keep their meaning (a group), and brace expansion stays rejected. Tests with the examples above, checked in both shells.

## nogo
They stay rejected; `/bin/sh -c` runs these strings.

## refs
`ShellSubsetLarge.pm`; large spec, section 3; `large-subset-tests.pl`.

@@ P2 | Parser additions | arithmetic | Accept `$( … )`, backquotes and `${#NAME}` inside `$(( … ))`
## now
Example: `echo $(($(date +%s) / 60))` and `echo $((${#HOME} > 0))`. Both shells expand the inner part first and then compute (`echo $(($(echo 6) / 2))` prints `3` in both). Our parser rejects them: `bad arithmetic expression` (7 samples).

## plan
**Done in 0.03** (your ruling in chat). Accept command substitutions and the `${…}` forms already allowed elsewhere inside `$(( ))`; the executor expands them before it computes, as it already does for `$x`. Tests in both shells. Needs a check that the executor's arithmetic accepts the tree (posix_spawn session; I test it on its bundle first).

## nogo
They stay rejected.

## refs
`ShellSubsetLarge.pm`; large spec, sections 3 and 4.

@@ P3 | Parser additions | variables | Allow setting `PS1`, `PS4`, `HOSTNAME`, `PIPESTATUS` and `BASH`
## now
Our parser rejects every assignment to a name the shell sets itself (rule U5 of the large spec). For these five names both shells behave the same: `PS1='> '; echo "[$PS1]"` prints `[> ]` in both; `HOSTNAME=h`, `PIPESTATUS=1`, `BASH=x` the same; `PS4='+ '; set -x; true` prints `+ true` in both. 30 samples within the lower yardstick, most `PS1=…` (20).

For three other names the shells differ, and they stay rejected: `_=x; echo "[$_]"` (dash `[x]`, bash `[]`), `LINENO=5` (bash keeps counting lines), `PPID=1` (bash: `PPID: readonly variable`, status 127).

## plan
**Done in 0.03** (your ruling in chat). Take the five names off the list of rule U5. Tests for both kinds.

## nogo
All stay rejected.

## refs
`ShellSubsetLarge.pm` (`%SH`); large spec, section 2 (U5).

@@ P4 | Parser additions | builtins | Accept `export $v=…`, `exit N` above 255, `umask -S` and `command -p`
## now
Our parser accepts these builtins only with literal names and plain forms. These four forms run alike in both shells:

- `v=x; export $v=1; echo "[$x]"` prints `[1]` in both (the name comes from an expansion).
- `exit 1000` ends with status 232 (1000 modulo 256) in both.
- `umask -S` prints `u=rwx,g=rx,o=rx` in both (with umask 022).
- `command -p ls /` runs `ls` with the default `PATH` in both.

These that look alike differ and stay rejected: `exit -1` (dash: `Illegal number`, 2; bash: 255), `local -r x=1` (dash: `bad variable name`), `unset -n`, `wait -n` (dash: `Illegal option`), `export -p` and `readonly -p` (listings).

## plan
**Done in 0.03** (your ruling in chat). Accept the four forms. `export $v=…` needs the executor to split the name from the expanded word at run time; `exit N` the status modulo 256; `umask -S` the symbolic output; `command -p` a default `PATH`. Parser and executor work, tested in both shells.

## nogo
They stay rejected.

## refs
`ShellSubsetLarge.pm`; `lib/ShellSpawn/Large.pm` (posix_spawn session); large spec, section 3.

@@ P5 | Parser additions | test | Let the program `[` (coreutils) decide what `[` and `test` do
## now
The subset runs `[` and `test` as the program `/usr/bin/[` of coreutils, not as dash's builtin (as it does for `echo`, decision K2 of "Shell Dialect Choices"). They differ:

- `[ a == a ]`: dash error (2); bash and coreutils 0.
- `[ -N f ]`: dash error; bash and coreutils work.
- `[ -v HOME ]` (is the variable set), `[ -a f ]` (unary `-a`: the file exists), `[ -R NAME ]` (is the variable a name reference): bash works; dash and coreutils error.
- `[ a \< b ]`: dash and bash 0; coreutils error (see R1).

Your proposal: in this testing context, the program wins over the shells' builtins.

## plan
Write this down as a rule: `[` and `test` are the program; the subset accepts them in all forms, except `<` and `>` (R1: a caller's option). The yardsticks (R3) then count ShellCheck's warnings about `test` (SC3014–SC3017, SC3062–SC3064) as excused, like `echo`. Spec text and tests; no executor change.

## nogo
`[` and `test` become an item of "Shell Dialect Choices", with the default there (reject the differing forms).

## refs
Large spec, section 2; `ShellSubsetLarge.pm`; `large-subset-yardsticks.pl`.

@@ P6 | Parser additions | agree | Accept more forms that both shells run alike: `read -p`, `cd -P`, `cd -L`, a pattern as a redirection target, shared `set -o` options, `trap` with lower-case signal names or numbers
## now
The new yardsticks list the samples that our parser rejects although both shells parse them and no difference between the shells is known: 304 samples. Besides the builtins of items B1–B3 and the cut commands of item Y2 (both on this page), these forms stand out:
- `read -p PROMPT NAME` (30 samples): dash 0.5.12 has `-p` too.
- `cd -P DIR` and `cd -L DIR` (7): both shells have both options.
- a pattern as a redirection target, as in `ls >*.log` (4): bash in POSIX mode does not expand it either (its manual, "Bash POSIX Mode", item 13), so both write to a file named `*.log`.
- `set -o OPTION` for an option both shells have, such as `set -o nounset` (12 samples, together with `set -o` alone; that one lists the options and stays rejected).
- `trap ACTION int` (a lower-case signal name) and `trap ACTION 8` (a signal number): 6 samples together.

## plan
Probe each form in both shells first. Then accept those where both agree, with tests. A form where the probe shows a difference instead gets a verdict in `doc/large-subset/tools/large-subset-verdicts.txt` for you to rule on, or an item on the page "Shell Dialect Choices".

## nogo
They stay rejected.

## refs
`Proc::ShellSubset::Large`; `doc/large-subset/large-subset-state.md`, section 4.

@@ B1 | Builtins left to sh | alias | Run `alias` and `unalias`, with aliases expanded like the shells do
## now
`alias NAME=VALUE` makes `NAME` stand for `VALUE` as the first word of later commands. Both shells expand an alias only in lines read **after** the line that defined it: `alias ll="echo L"` on one line and `ll x` on the next prints `L x` in both; on the same line (`alias ll="echo L"; ll x`) `ll` is not expanded, and both print `ll: not found` with status 127. Both also expand aliases in `eval` text and in files read by the builtin `.`. Our parser rejects `alias` and `unalias` (`builtin 'alias'`): 102 samples within the lower yardstick, most only defining an alias (`alias ll='ls -l'`).

## plan
Your idea: treat aliases like functions. The parser reads the whole string at once, so it expands a literal alias definition in the lines that follow it, as the shells do, and records the definitions. At run time the executor keeps the alias table, and expands aliases in `eval` text and `.` files. Where an alias cannot be known when parsing (a name or value from an expansion), the string is rejected, or stops with status 248 at run time. Listing (`alias` alone) prints dash's format. Tests in both shells. Needs the executor (posix_spawn session) and the fix of L10 (Perl's own output written without blocking; see B3).

## nogo
`alias` and `unalias` stay rejected; `/bin/sh -c` runs these strings.

## refs
`ShellSubsetLarge.pm`; `lib/ShellSpawn/Large.pm`; large spec, sections 3 and 5.

@@ B2 | Builtins left to sh | interactive | Let interactive-only builtins do nothing, by a caller's option
## now
Some builtins only make sense in an interactive shell: `history`, `fc`, `bind`, `help`, `compopt`, `logout`, and `set -o history`. (`set -o vi` and `set -o emacs` work in both shells, status 0; our parser rejects them too.) dash has none of the first six: `history; echo $?` prints `history: not found` and `127`. bash prints nothing for `history` and returns 0 (in `sh -c` there is no history); `bind` warns that line editing is off. Our parser rejects them all (107 samples within the lower yardstick; `set -o history` lies outside the yardsticks, see Y1).

## plan
A caller's option, `interactive`, with three values: `reject` (as today), `nop` (accept; each does nothing and returns 0, as bash mostly does), or `dash` (accept; behave as dash: `NAME: not found`, status 127). Default: `nop` for the large subset, `reject` for the base parser `ShellSubset` (as you proposed for the builtins module). The NOPs live in that builtins module: a planned Perl module, one function per builtin, where a builtin counts as implemented when its function exists. `set -o vi` and `set -o emacs` become plain NOPs.

## nogo
They stay rejected.

## refs
`ShellSubsetLarge.pm`; the builtins module (new, posix_spawn session); large spec, sections 3 and 5.

@@ B3 | Builtins left to sh | jobs | Run `jobs`, `bg`, `fg`, `hash`, `type`, `times` and reading `ulimit` as dash does
## now
These builtins work in a non-interactive shell, and our parser rejects them (50 samples within the lower yardstick): `jobs` lists background jobs (empty, status 0, in both shells when none run); `bg` and `fg` fail without job control (dash `No current job`, 2; bash `no job control`, 1); `hash NAME` looks a program up (`hash nosuch`: `not found`, status 1, in both); `type ls` prints `ls is /usr/bin/ls` in both (before `ls` has run; afterwards dash says `tracked alias`, bash `hashed`); `times` prints the times used (format differs); `ulimit -n` prints a limit (the same in both).

## plan
Implement them in the builtins module, as dash does (rule A5 of the large spec: where the shells differ at run time, follow dash): `jobs` from the executor's job table, `bg`/`fg` dash's error, `hash` a `PATH` lookup, `type` dash's texts, `times` dash's format, `ulimit` only for reading (`ulimit -n`, `ulimit -Hn`); setting a limit stays rejected, since it would change the Perl process itself. Needs the fix of L10 first (output from Perl written without blocking).

## nogo
They stay rejected.

## refs
The builtins module (new); `lib/ShellSpawn/Large.pm`; large spec, sections 3 and 5; `large-subset-upstream-reports.md` (L10).

@@ Q1 | Questions for you | rules | Keep `kill -SIGHUP` working through the program `kill` (the program's rule first)
## now
Two of your rules meet here:
- Where both shells refuse a form, the subset refuses it too.
- For the commands the subset runs as programs, the program decides.

Example: `kill -SIGHUP PID`. dash refuses it (`kill: Illegal option -S`). bash refuses it too, as its manual says ("Bash POSIX Mode", item 38). The subset runs the program `/usr/bin/kill`, which accepts the `SIG` prefix, so today the signal is sent.

Claude suggests No-go here, so that "both shells refuse" comes first.

## plan
The program's rule comes first: `kill -SIGHUP PID` keeps working, as today. `doc/large-subset/large-subset-state.md` records this order.

## nogo
"Both shells refuse" comes first: the parser rejects `kill` with an argument that starts with `-SIG`, with a test checked in both shells.

## refs
`doc/large-subset/large-subset-state.md`, section 2; `Proc::ShellSubset::Large`.

@@ Q2 | Questions for you | rules | Keep `printf -v` accepted, with what the program `printf` makes of it
## now
`printf -v x %s hi`: bash sets the variable `x` to `hi`; dash fails (`Illegal option -v`). The subset runs the coreutils program `printf`, which prints `-v`, warns about excess arguments, and sets nothing. Under the rule "the program decides", the subset accepts this today, although the result matches neither shell and serves no purpose.

Claude suggests No-go here.

## plan
`printf -v` stays accepted, with the program's output.

## nogo
The parser rejects `printf -v`, as dash does, with a test. Only this form changes. Forms where the program fails like dash and only bash differs, such as `[ -v NAME ]`, stay as the program makes them. `printf %q` stays too: the program and bash both quote, only differently (`'a b'` and `a\ b`), and dash fails.

## refs
`doc/large-subset/large-subset-state.md`, section 2.

@@ Q3 | Questions for you | verdicts | Adopt Claude's readings of the four open verdicts
## now
For these four differences, no probe and no manual decides between blatant and subtle. They count as open (outside the lower yardstick, inside the upper one) until you decide:
- **A.** `local` outside a function: both shells print an error; then dash stops, and bash goes on with the rest. `return` outside a function: dash ends quietly with status 0; bash prints an error and exits with status 2. Claude's reading: blatant. For `local`, because the rest of the string runs in bash only; but since both shells print an error, you may prefer the rule "both shells refuse: we refuse too", which gives the same result for the subset (it rejects the form). For `return`, because the statuses (0 and 2) and the error message differ.
- **B.** `$-` (the letters of the shell's options): empty in `dash -c`, `hBc` in bash. A test for `*i*` (is the shell interactive?) gives the same answer in both. Claude's reading: blatant for every use of `$-`, because the detector cannot tell the printing from the test.
- **C.** `$LINENO`: empty in `dash -c` strings, `1` in bash. Whether dash's manual promises a value there was not checked: its manual page is not installed in Claude's workspace. Claude's reading: blatant.
- **D.** `x=$(cmd) 2>/dev/null`, an assignment with a command substitution plus a redirection: dash redirects before it runs `cmd`, bash after. The outputs differ only when `cmd` writes to the redirected descriptor. Claude's reading: subtle.

## plan
Record the four readings in `doc/large-subset/tools/large-subset-verdicts.txt` (A, B and C blatant; D subtle), and recompute the figures. To change one, write its letter and your verdict in a note.

## nogo
They stay open.

## refs
`doc/large-subset/tools/large-subset-verdicts.txt`.
