# G-015 Punk executable `script` subcommand: reliable non-interactive piped/script execution Status: achieved 2026-07-07 Scope: src/vfs/_config/punk_main.tcl, src/lib/app-punkshell/punkshell.tcl (script path or a leaner dedicated app package) Goal: ` script [] [...]` executes a script file or piped stdin content (scriptname optional when input is piped) in an interp preloaded with the basic punk modules and aliases a punk shell provides by default, and always terminates with the script's success/failure as its exit code - without the `shell` subcommand's shellfilter channel transforms/logging stacks and without ever dropping into an interactive shell - so agents can reliably make piped script calls to punk executables. Acceptance: piping commands to ` script` runs them and terminates at stdin EOF with no trailing `exit` required, exit code 0 on success; a failing piped command terminates the process with a nonzero exit code and the error on stderr, never landing in an interactive shell regardless of console availability or PUNK_PIPE_EOF; ` script [...]` executes the file with conventional ::argv0/::argv and propagates its error status the same way; the script path installs none of the `shell` subcommand's shellfilter stacks/transforms and the launch plumbing itself emits nothing on stdout/stderr (the current stub's stderr diagnostics removed) so exec-style callers see only the script's own output; the motivating example works with no package require boilerplate: piping `dev projects.work **` to ` script` emits the matching-project table and exits 0, because the script interp carries the default punk shell module/alias environment. ## Context Agents (and exec-style callers generally) currently have no reliable way to run a one-shot command against a punk executable. The working pattern is to pipe repl input into the default shell path, e.g. (powershell): 'dev projects.work *tomlish*;exit' | punk902z This is fragile in two ways: 1. The trailing `exit` is load-bearing: without it, stdin EOF triggers the app-punkshell eof handling, whose default heuristic policy attempts to reopen the console (`CONIN$` / `/dev/tty`) and drop into an interactive repl. When a console exists but no human is watching (typical agent/CI harness), that is a permanent hang. `PUNK_PIPE_EOF=exit` mitigates, but callers must know to set it. 2. If any command before the `exit` errors, the repl keeps consuming input and the `exit` line may never run as intended - the session can still land interactive, and the process exit code does not reflect the failure. Meanwhile the `script` subcommand in the punk_main.tcl dispatch (src/vfs/_config/punk_main.tcl ~1163) is an incomplete stub: it prints stderr diagnostics ("main.tcl launching script with args: ...") and sets ::tcl_interactive, but never actually executes anything. Note also the dispatch default (~1090): any first argument that is not a recognised mode/subcommand is reclassified as `script`, so completing this path also completes the bare ` ` form. The `shell` subcommand path (app-punkshell) wraps the session in shellfilter channel stacks (`punkshellout`/`punkshellerr` with file/syslog logging targets, plus ansiwrap on stderr in places). Those transforms exist for interactive-shell ergonomics and logging; for a one-shot script call they add overhead, alter output, and risk stderr chatter - which matters because Tcl `exec` treats any stderr output as command failure by default (see the comment block in punk_main.tcl's `punk` subcommand case). ## Approach - **Decided 2026-07-07: a leaner dedicated app package** (not a mode of app-punkshell). Rationale: the acceptance is defined by absence (no shellfilter stacks, no logging side effects, no interactive fallback) which a package that never loads that machinery guarantees by construction; app-punkshell's load-time behaviour (shellfilter stacks with syslog/file targets writing punkshell_out.log/punkshell_err.log into the caller's cwd, logging threads) is wrong for one-shot calls; and isolation caps the blast radius of shell-path machinery (the 2026-07-06 silent-stdout failure lived in exactly the code reuse would couple to). Duplication mitigation: the substantive shared piece - the definition of the default punk shell module/alias environment - is factored into a shared module-level definition (punk::aliascore as the seed) consumed by both app packages; launcher control flow is deliberately not shared (share definitions via modules, not control flow). This package shape also previews the G-031 registration model (subcommand -> small handler package), as app-tomlish demonstrated. - ` script [...]` - execute the file with conventional ::argv0/::argv semantics, exit with its status. - `... | script` - read the whole of stdin as the script when no scriptname is supplied and stdin is piped/redirected; terminate at EOF, never reopening a console. The scriptname form must still work with piped stdin present (the script may itself want to read stdin) - "with or without a trailing scriptname". - No shellfilter stacks/transforms on the script path; plain channels. - No launcher output of its own on stdout or stderr (remove the stub diagnostics); the script's own output is the only output. - Errors: report on stderr, exit nonzero. Success: exit 0 (or the script's own explicit exit code). No interactive fallback on this subcommand under any eof/error condition - PUNK_PIPE_EOF and console availability are irrelevant here by design. - The script interp loads the basic punk modules and aliases that are available by default in an interactive punk shell, so commands like `dev projects.work` work in a piped one-liner with no `package require` boilerplate. This is deliberately not the leanest environment for basic scripts - fast/bare script execution is the future province of an expanded `tclsh` subcommand, out of scope here. The exact recommended one-liner gets documented under G-017. ## Alternatives considered - Hardening the shell/repl pipe path instead (PUNK_PIPE_EOF, error-aborts-input) - insufficient alone: the shell path's channel transforms and repl semantics are wrong for exec-style callers even when eof/error policy is fixed; a dedicated clean path is simpler than making the interactive path dual-purpose. - Requiring agents to set PUNK_PIPE_EOF=exit and keep the trailing `exit` - rejected as the durable answer: it leaves exit codes meaningless on mid-script errors and depends on every caller knowing the incantation. - A bare interp (no punk modules/aliases preloaded) for the script path - rejected for this subcommand: the point is that shell-default commands (`dev projects.work` etc.) work as piped one-liners; lean bare-script execution belongs to a later expansion of the `tclsh` subcommand. ## Notes - Achieved-status detail (restored 2026-07-11; this text originally rode the Status: line as a parenthetical and was dropped when the 2026-07-11 two-tier index restructure normalised detail-file Status lines to the bare index form): implemented as src/lib/app-punkscript; verified on punk902z/tcl9 and punksys/tcl8.6 kits: piped success/error/exit-code cases, file form with args and stdin passthrough, and the motivating `dev projects.work *tomlish*` one-liner - table emitted, exit 0, no boilerplate. Stdin form additionally echoes the script's final result when non-empty, giving return-value commands one-liner ergonomics; file form keeps pure script semantics. Terminal-stdin-with-no-args yields a usage error by design - spot-check from a real console typed session remains a manual item. - Result-shape observation (2026-07-07): `shellrun::runx -tcl` returns a dict keyed {result stdout stderr} with NO exitcode key, unlike the exec-mode result (exitinfo with exitcode) - callers doing `dict get $result exitcode` error. Possibly by design for in-process eval, but the script-path work here should document (or normalise) the result shapes agents will consume. - The eof policy machinery (PUNK_PIPE_EOF exit/interactive/heuristic) in app-punkshell (~punkshell.tcl 466-507) stays as-is for the `shell` subcommand; this goal does not change interactive-shell behaviour. - Related later goal: G-016 extends what the motivating `projects.work` call can find; G-017 documents the agent-facing invocation pattern once this lands.