You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

8.4 KiB

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: <punkexe> script [<scriptname>] [<args>...] 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 <punkexe> 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; <punkexe> script <file> [<args>...] 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 *<name>* to <punkexe> 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 <punkexe> <somefile> 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.

  • <punkexe> script <file> [<args>...] - execute the file with conventional ::argv0/::argv semantics, exit with its status.

  • ... | <punkexe> 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.