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

%% GROUP Yardsticks and corpus
How the corpus figures are computed. Y1 is a set of judgements for you to check; Y2 and Y3 clean the corpus. Nothing here changes the parser.

%% GROUP Parser additions
P1âP4: forms that dash and bash run alike but `ShellSubsetLarge` rejects today; P5 settles forms where they differ. Each is a change to the large grammar, with tests; P2 and P4 also need the executor (the posix_spawn session's code that runs the parsed strings).

%% GROUP 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: upper ("nothing bogus found") and lower ("compatible")
## now
Your messages: we need two yardsticks, one for "both accept" and one that also leaves out what is incompatible in subtler ways (between the shells, or in `test`); and "both accept" means both parse the string **and** "we have not found out that one of them produces bogus results", bogus meaning blatantly not matching its docs or the other shell.

## plan
Done, in `large-subset-yardsticks.pl`. A sample is within the **upper yardstick** when `dash -n` and `bash --posix -n` both accept it, ShellCheck (`shellcheck -s dash`) finds no construct dash lacks (its warnings SC3xxx), and our parser did not reject it for a form where one shell errors or gives a blatantly different result. It is within the **lower yardstick** when, in addition, our parser did not reject it for a subtle difference either. Which rejections count as blatant and which as subtle is item Y1. The figures are in the table at the top of this page.

## nogo
Not applicable: this is your decision.

## refs
`claude/large-subset-ceiling.md` (to be updated with the two yardsticks); `large-subset-yardsticks.pl` (new).

@@ Y1 | Yardsticks and corpus | judgement | Count these rejections as "blatant" (outside both yardsticks) or "subtle" (outside the lower one only)
## now
Each rejection by our parser has a message. For the yardsticks (R3) I sorted the messages that name a difference between the shells into two kinds. **Blatant**: one shell errors, or the result is clearly different. **Subtle**: both run it, and the difference shows only in details. My sorting, with the number of samples, counted over all sources, including samples outside the yardsticks:

- Blatant: arrays such as `${#a[*]}` (124; dash: `Bad substitution`); `((` at the start of a command (50; dash runs a nested subshell); `set` options dash does not have, such as `-o history`, `-o pipefail`, `-B`, `-H` (30; dash errors; `-o vi` and `-o emacs` work in both and are no difference); an input redirection without a command, as in `$(<file)` (21; dash gives nothing, bash the file's text); signal names such as `SIGINT` or `ERR` in `trap` (18; dash: `trap: SIGINT: bad trap`, status 1); `NAME[1]=x` (12; dash runs a command of that name); `cmd &>f` (10; dash runs `cmd` in the background); `return` and `local` outside a function (9; `return`: dash ends the string quietly, bash prints an error and goes on; `local`: both print an error, but only dash stops). `break` and `continue` outside a loop do the same in both (nothing, status 0) and are no difference; `>& file` (5; dash errors).
- Subtle: arguments of `read` other than plain names, mostly `read -p PROMPT` (112; the item G18 of "Shell Dialect Choices"); listing forms such as `set` or `export` alone (26; the text of the listing differs); `[^…]` in a pattern (15; G14); `.*` (8; G2); `set -x`, `set -v` (5; the trace text differs, G9); `local x` without a value (4; G1); `.` with arguments (4; bash passes them, dash ignores them); a function named like a builtin (4; G17); `$@` joined (3; G3, G4); an assignment with a redirection and a `$` (2): `x=$(echo e >&2; echo v) 2>/dev/null` hides the `e` in dash, which redirects first, and shows it in bash, which expands first.

## plan
Keep this sorting. Move any message by writing it in a note ("`read -p` is blatant"); I then recompute and update the table at the top.

## nogo
Tell me in a note which sorting you want instead, or that the split should not depend on my judgement: then I count every difference our parser names as blatant, and the two yardsticks coincide.

## refs
`large-subset-yardsticks.pl` (the lists `@BLATANT` and `@SUBTLE`); `claude/large-subset-ceiling.md`.

@@ Y2 | Yardsticks and corpus | NL2Bash | Leave out NL2Bash commands cut after a continuation backslash
## now
NL2Bash (a collection of one-line commands taken from scripts) has 15 commands that end in a backslash: the line was cut where the original continued it on the next line. Example:

```
find . -inum $inum -exec rm {} \
```

Both shells parse it (a backslash at the very end stays a literal `\`: `echo a\` prints `a\`), but the command is incomplete: here `find` gets `\` in place of the `;` that ends `-exec`. Our parser rejects it with `final backslash`.

## plan
Leave them out of the measurement, with the reason "NL2Bash: cut after a continuation backslash" recorded in `drops.tsv` (the list of left-out samples with their reasons), as for duplicates and tldr's key notation.

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

## refs
`large-subset-drops.pl`; `claude/large-subset-ceiling.md`, section 2.

@@ 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
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
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
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
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`.

@@ 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).
