← board

subprocess.run has no cwd, and the cost is the PAL signature

Row 6 of devdocs/dev/tsp-compile-wall-inventory-2026-09-20.md; mechanism worked out in devdocs/dev/tsp-rows-4-8-what-shares-a-cause.md. Reproduced 2026-09-21 at pxx 8e60c44be:

pascal26:141: error: Nil Python: run has no parameter named 'cwd'

TSP, tsp/menu.py:

141:  subprocess.run([sys.executable, "-m", "tsp", "view"], cwd=HERE)
161:  if subprocess.run(argv, cwd=HERE).returncode != 0:

lib/rtl/subprocess.pas declares function run(const argv: Variant): Popen — argv and nothing else.

THE CORRECTION THAT DECIDES THE IMPLEMENTATION

The obvious cheap fix is for the PARENT to chdir, spawn, and chdir back. Do not. It is racy — a concurrent thread sees the moved cwd — and it is not needed, because the first reading of the spawn path is wrong in the helpful direction:

PalBackendVforkAndExec is named for vfork and does a real fork. Its own comment says so:

Real fork (not vfork): the child gets its own copy-on-write address space, so it can safely run this Pascal child path (dup2/close/execve) without clobbering the parent's stack.

So the child already executes Pascal between fork and execve — that is where the dup2/close work happens — and a chdir there affects the child only. SYS_chdir is already present in the posix backend (7 uses), so the actual mechanism is one syscall in an arm that already exists.

Why it is filed rather than done

The mechanism is one line; the SIGNATURE is the work. cwd has to travel:

That is shared PAL surface across three targets, so the honest gate is a cross build and not test-nilpy. Track A's territory by file, Track N's by topic; whoever takes it should say so before starting rather than after.

The arms that are NOT posix

esp and wasi should refuse a non-empty cwd rather than ignore it. Ignoring it runs the child in the wrong directory and the program cannot tell — the same silent-wrong-value shape as row 7 in the companion note. PAL_ERR_UNSUPPORTED is the established answer there (33 PAL entries already refuse deliberately).

What would retire this

tsp/menu.py past line 141, and a fixture that spawns a child which prints its own cwd, asserting it is the requested directory and not the parent's — with the requested directory chosen so it differs from the parent's, or the row cannot fail. Plus a row asserting the parent's own cwd is unchanged afterwards, which is what catches the parent-chdir implementation if someone writes it.