Browse Source

punkexe -e one-liner support (G-077): app-punkscript 1.1 + boot core 0.4.0 top-level carve-out + moduledoc 0.5.0 (0.50.0)

- app-punkscript 1.0 -> 1.1: 'script -e <script> ?args...?' - ::argv0 '-e', trailing args in ::argv, errorInfo to stderr + exit 1 on error, usage error + exit 1 when no script follows (never interactive fall-through, never stock's argv-swallow + stdin-read misparse); stdin never read by the form itself; a non-empty final result is echoed (stdin-form one-shot ergonomics, so return-valued one-liners like 'dev projects.work *x*' emit)
- boot core 0.3.0 -> 0.4.0: a top-level first argument of exactly '-e' reclassifies to 'script -e' ahead of the G-032 unknown-first-arg refusal on script-default kits (tool-style kits keep '-e' routed to their processor; '<punkexe> tclsh -e ...' keeps deliberate stock parity per G-118); the unknown-first refusal hint and the degraded plain help now name the -e forms
- moduledoc punkexe 0.4.0 -> 0.5.0: (script)::punkexe::script declares the oneliner @form (both forms explicitly named - punk::args renames _default to the FIRST named @form args-included); top-level prose documents the reclassification; tclsh boundary text points at the live forms
- tests: scriptexec.test gains 10 -e pins (both surfaces, argv0/argv, piped stdin read+unread, no-arg usage error on both surfaces, errorInfo+exit 1, result echo, tclsh -e stock-parity boundary); launcherhelp.test pins the refusal hint's -e pointer; 49/49 green on the rebaked punk902z/punksys/punk86 (stamps boot_core=0.4.0); the named acceptance invocations and the full matrix also run by hand on the baked kits
- DOX: -e bullets in src/vfs, src/lib, src/tests/shell and bin AGENTS.md; root AGENTS.md tclsh -e pitfall parenthetical now records the live punk-kit forms + the ad-hoc 'src script -e' option; README script-subcommand bullet; ARCHITECTURE.md entry-point/script/tclsh sections (G-077 leaves the in-flux list)
- CHANGELOG + punkproject 0.50.0
Assisted-by: harness=opencode; primary-model=openrouter/moonshotai/kimi-k3; api-location=openrouter.ai
master
Julian Noble 6 days ago
parent
commit
5a24503544
  1. 4
      AGENTS.md
  2. 8
      ARCHITECTURE.md
  3. 19
      CHANGELOG.md
  4. 1
      README.md
  5. 2
      bin/AGENTS.md
  6. 2
      punkproject.toml
  7. 2
      src/lib/AGENTS.md
  8. 2
      src/lib/app-punkscript/pkgIndex.tcl
  9. 39
      src/lib/app-punkscript/punkscript.tcl
  10. 36
      src/modules/punk/args/moduledoc/punkexe-999999.0a1.0.tm
  11. 3
      src/modules/punk/args/moduledoc/punkexe-buildversion.txt
  12. 2
      src/tests/shell/AGENTS.md
  13. 4
      src/tests/shell/testsuites/punkexe/launcherhelp.test
  14. 104
      src/tests/shell/testsuites/punkexe/scriptexec.test
  15. 1
      src/vfs/AGENTS.md
  16. 46
      src/vfs/_config/punkboot_core.tcl

4
AGENTS.md

@ -97,8 +97,8 @@ When the user requests a durable behavior change, record it here or in the relev
- cmd.exe PATH truncation (this machine, and any Windows machine with a heavily populated PATH): cmd.exe truncates a long PATH, so a tool that resolves fine in PowerShell may be "not found" when invoked via `cmd.exe /c`. Use absolute executable paths inside any `cmd /c` command line, and prefer PowerShell-native invocation unless a console host is specifically required (e.g. hidden-console test harnesses). If a tool is missing only under cmd.exe, suspect truncation before absence.
- Windows paths under POSIX shells (WSL, msys/git-bash): a `C:/...` path passed unconverted to a POSIX file command (`mkdir -p`, `cp`, output redirects) is a RELATIVE path - it silently creates a stray directory tree in the cwd whose first component is literally named `C:` (the NTFS-illegal `:` is stored as private-use character U+F03A, so it renders as `C` plus an odd dot/glyph in Explorer, and while the tree contains only empty directories it is invisible to git and fossil). Translate at the boundary (`wslpath` for WSL, `cygpath` for msys - msys auto-conversion does NOT cover paths inside quoted command strings or when MSYS_NO_PATHCONV/MSYS2_ARG_CONV_EXCL is set), and after WSL/msys-driven work glance at the repo root for a stray `C*` entry. (Origin: G-140 WSL smokes leaked an empty temp-dir skeleton into the repo root, found and removed 2026-07-31.)
- Agent-authored text is plain ASCII by default: no em/en dashes, curly quotes, arrow or ellipsis characters, or other typographic Unicode - use ASCII equivalents (" - ", straight quotes, "->", "..."). The rule governs elements the agent generates, with extra force for outward-bound artifacts (ticket drafts, bug reports, emails, commit messages - anything likely to be pasted into an external system), which are verified before handover (e.g. grep for `[^\x00-\x7F]`). The exceptions are illustrative, not a closed list: non-ASCII subject matter (encoding/Unicode/ANSI-art test data, or documentation demonstrating such behaviour), verbatim quotes of existing material, and explicit user request are the common cases, but any good reason qualifies - state the reason when deviating. Content the agent did not author is outside the rule: existing files are never bulk-retrofitted, and while checking non-authored content (e.g. files being committed on the user's behalf) is fine, non-ASCII there is the author's prerogative - if unsure whether it is intentional, stop and ask rather than fix or block.
- Tcl has no `-e`/`-c` one-liner flag (a reflex agents carry over from perl/python/node). Stock `tclsh`/`tclsh86`/`tclsh90` recognise only `-encoding name` as a leading option; any other argument starting with `-` is NOT treated as a script file - all arguments land in `$argv` and tclsh reads commands from stdin. On a console that hangs forever at an interactive prompt; with piped/redirected stdin it exits 0 having silently ignored the supposed one-liner and executed stdin instead. (The punk kits differ: they treat `-e` as a script filename and error out immediately - no hang, but still no one-liner.) To run ad-hoc Tcl: write a temp `.tcl` file and run `tclsh path/to/file.tcl`, or pipe the script to stdin (`echo 'puts hi' | tclsh`, or a bash heredoc). Defensive habit regardless: when exec'ing tclsh non-interactively, redirect stdin (`</dev/null`, `< NUL`) so a mis-invocation exits at EOF instead of hanging.
- For ad-hoc punkshell scripts that need working-tree modules, use `bin/punk91 src script` for stdin scripts or `bin/punk91 src script path/to/file.tcl` for script files. Do not use `bin/punk91 src` for piped/redirected snippets: `src` alone starts the live shell on the repo source tree and may execute stdin input but then remain in shell/eof handling instead of exiting cleanly. Keep `src` before `script` so the working-tree modules are loaded.
- Tcl has no `-e`/`-c` one-liner flag (a reflex agents carry over from perl/python/node). Stock `tclsh`/`tclsh86`/`tclsh90` recognise only `-encoding name` as a leading option; any other argument starting with `-` is NOT treated as a script file - all arguments land in `$argv` and tclsh reads commands from stdin. On a console that hangs forever at an interactive prompt; with piped/redirected stdin it exits 0 having silently ignored the supposed one-liner and executed stdin instead. (The punk kits differ since G-077, achieved 2026-08-04: `<punkexe> -e <script> ?args...?` - and `<punkexe> script -e <script> ?args...?` - run the code as a one-liner with honest exit codes, so the reflex works there; stock `tclsh` still has no such flag.) To run ad-hoc Tcl: write a temp `.tcl` file and run `tclsh path/to/file.tcl`, or pipe the script to stdin (`echo 'puts hi' | tclsh`, or a bash heredoc). Defensive habit regardless: when exec'ing tclsh non-interactively, redirect stdin (`</dev/null`, `< NUL`) so a mis-invocation exits at EOF instead of hanging.
- For ad-hoc punkshell scripts that need working-tree modules, use `bin/punk91 src script` for stdin scripts, `bin/punk91 src script path/to/file.tcl` for script files, or `bin/punk91 src script -e '<code>'` for one-liners. Do not use `bin/punk91 src` for piped/redirected snippets: `src` alone starts the live shell on the repo source tree and may execute stdin input but then remain in shell/eof handling instead of exiting cleanly. Keep `src` before `script` so the working-tree modules are loaded.
- VCS ignore rules are dual-tracked (git + fossil). An agent that changes ignore rules in one VCS must make the equivalent change in the other in the same work unit: `.gitignore` is the canonical statement of intent and `.fossil-settings/ignore-glob` is hand-derived from it, never the reverse - so a `.gitignore` edit includes deriving the ignore-glob translation, and an ignore-glob-only edit is wrong unless it is purely catching up to `.gitignore`. Fossil globs differ semantically (no negation - tracked exceptions need explicit `fossil add`; case-sensitive; `*` crosses `/`): translate per the derivation rules in `.fossil-settings/AGENTS.md` and run its verification checks before committing.
- Dual-VCS commit cadence (user, 2026-07-29): in a tree tracked by both git and fossil (this repo; git is primary), agents commit each change-set to BOTH VCS in the same work unit by default - a request to commit means both, and only an explicit single-VCS instruction narrows it. The fossil checkin mirrors the git commit (same message body; new/renamed files need explicit `fossil add` / `fossil mv`). `fossil changes` never lists untracked files: a new file git-added but missed by its mirrored checkin is invisible there and surfaces only in `fossil extras`, which is normally empty in this tree - glance at it before finishing a dual-VCS work unit and treat any entry as drift to disposition (drift instance 2026-08-01: two .test files git-added in the G-123/G-127 increments, never fossil-added). Before the fossil checkin, run `fossil changes` and look for pending material beyond the current change-set: the developer may commit to git manually and let fossil lag, and a plain `fossil commit` sweeps everything pending - when lag is present, either land a separate catch-up checkin first, or fold the lag in with a message naming the covered git commits (per-change rationale stays in git history; the catch-up message summarizes). Coexistence mechanics (ignore sync, CRLF prompt avoidance, tracked-set verification) are in `.fossil-settings/AGENTS.md`.
- Throwaway fossil repositories (test/experiment repos an agent creates, e.g. in a session scratchpad) must not register in the user's real global fossil config-db: `fossil init`/`fossil open` write persistent `repo:`/`ckout:` rows into `%LOCALAPPDATA%\_fossil`, which is the enumeration source for `dev projects.work` project discovery (G-016/G-017). Set `FOSSIL_HOME` to a disposable scratch directory for the duration of such fossil commands (both fossil and `punk::repo::fossil_get_configdb` honour it first). If pollution has already occurred: `fossil all ignore <repo-path>`, delete the directory, then any `fossil all` command prunes the orphaned `ckout:` row.

8
ARCHITECTURE.md

@ -42,12 +42,12 @@ app package from src/lib/ (app-punkshell, app-punkscript, app-repl, app-shellsp
## Boot and launch chain
- **Kit anatomy.** Each `src/vfs/*.vfs` folder is a runtime payload; `src/runtime/mapvfs.toml` (G-024, tomlish-parsed; deprecated `mapvfs.config` line format still readable) maps payload folders to platform runtimes under `bin/runtime/<platform>/` as named kit outputs, with named groups and generative version-named schemes; an optional sibling `src/vfs/<name>.vfs.toml` declares payload packages materialized INTO the folder with drop-in-wins precedence (G-115 - spec: `src/vfs/README.md`); `src/vfs/_vfscommon.vfs/` is a generated merge of common libraries (never hand-edited); `src/vfs/_config/` holds the entry scripts. Sources: `src/vfs/AGENTS.md`, `src/runtime/AGENTS.md`.
- **Entry point.** `src/vfs/_config/punk_main.tcl` (thin main sourcing the shared boot core - G-031). An optional first argument is a dash-delimited package mode composed of tokens from `minted`, `os`, `src`, `internal` (`internal` is always appended when absent; `minted` was named `dev` pre-0.43.0), optionally scoped with the `proj:` prefix (G-033: `minted`/`src` resolve against the project containing the cwd - walk-up to the nearest git/fossil repo root with a punkshell-style src tree - instead of the executable's own project; discovery is always reported, never a silent rebind). The next argument is the subcommand: `shell`, `script`, `punk`, `tclsh`, `shellspy`, `buildinfo`, `help`. A non-subcommand first argument naming an existing file (or `lib:*` reference) is treated as a script invocation; anything else is refused with usage (G-032); no arguments at all means interactive shell. Subcommands map to app packages under `src/lib/` (`app-punkshell`, `app-punkscript`, `app-repl`, `app-shellspy`). The launch surface self-documents through punk::args (G-032, achieved semantics in boot core 0.3.0): `help`/`-help`/`<subcommand> -help` render tabled usage from the moduledoc definitions plus project registrations (`::punkboot::project_subcommand_info` - summaries, definition ids, optional declared parse), degrading to a plain subcommand list when that stack is unavailable. Source: `src/vfs/AGENTS.md`.
- **Entry point.** `src/vfs/_config/punk_main.tcl` (thin main sourcing the shared boot core - G-031). An optional first argument is a dash-delimited package mode composed of tokens from `minted`, `os`, `src`, `internal` (`internal` is always appended when absent; `minted` was named `dev` pre-0.43.0), optionally scoped with the `proj:` prefix (G-033: `minted`/`src` resolve against the project containing the cwd - walk-up to the nearest git/fossil repo root with a punkshell-style src tree - instead of the executable's own project; discovery is always reported, never a silent rebind). The next argument is the subcommand: `shell`, `script`, `punk`, `tclsh`, `shellspy`, `buildinfo`, `help`. A non-subcommand first argument naming an existing file (or `lib:*` reference) is treated as a script invocation; a first argument of exactly `-e` runs the following argument as a Tcl one-liner (G-077, achieved - handled as `script -e`; tool-style kits keep `-e` routed to their own processor); anything else is refused with usage (G-032); no arguments at all means interactive shell. Subcommands map to app packages under `src/lib/` (`app-punkshell`, `app-punkscript`, `app-repl`, `app-shellspy`). The launch surface self-documents through punk::args (G-032, achieved semantics in boot core 0.4.0): `help`/`-help`/`<subcommand> -help` render tabled usage from the moduledoc definitions plus project registrations (`::punkboot::project_subcommand_info` - summaries, definition ids, optional declared parse), degrading to a plain subcommand list when that stack is unavailable. Source: `src/vfs/AGENTS.md`.
- **Package modes pick module provenance.** `internal` uses module snapshots baked into the kit at bake time; `minted` loads the mint output trees at repo-root `modules/` and `lib/`; `src` loads the unbuilt working tree (`src/modules`, `src/lib`, `src/bootsupport`, `src/vendormodules`) with `package prefer latest` so magic-version dev modules (`999999.0a1.0`) beat stamped snapshots. `src` mode is the standard way to verify working-tree changes without a rebuild. Source: `bin/AGENTS.md` "Launch package modes".
- **Payload mount derivation (G-129, achieved).** The boot keys zipfs presence on `tcl::zipfs::mount` and derives where the executable's attached archive actually mounted from the mount table itself (the entry whose archive file is the executable; fallback: the mount containing the boot script) instead of assuming `tcl::zipfs::root` + `//zipfs:/app`. Modern runtimes still answer `//zipfs:/app`; the androwish/undroidwish 8.6 backport has no `root` command, mounts at the executable's own path, and boots the same payload through its `app/main.tcl` in-archive hook (`src/vfs/punk8win.vfs` carries fauxlinks for both conventions). A mounted archive the boot cannot attribute is reported on stderr rather than silently yielding no internal paths. The derivation procs stay defined post-boot (`::punkboot::zipfs_kit_mountbase`). Sources: `src/vfs/AGENTS.md`, `bin/AGENTS.md` "Kit-wrappable runtime requirements", `src/tests/shell/testsuites/punkexe/kitmountpoint.test`.
- **Static package capture (G-058, achieved).** Boot probe-loads the runtime's statically linked packages in a throwaway interp and records what each provides; every interp the shell fabricates (boot, codethread, shellthread workers) seeds `package ifneeded <name> <ver> {load {} <prefix>}` from that record, and `punk::packagepreference` resolves static-vs-bundled version-aware. Sources: `src/vfs/AGENTS.md`, `goals/archive/G-058-static-runtime-packages.md`.
- **`script` subcommand (G-015, achieved).** The lean non-interactive path: default punk shell module/alias environment, no shellfilter stacks/transforms, honest exit codes, launch plumbing emits nothing on stdout/stderr (exec-style callers see only the script's own output). Supports `lib:<name>` scriptlib resolution. App package: `src/lib/app-punkscript/`.
- **`tclsh` subcommand (G-118, achieved).** Plain-tclsh emulation with NO punk modules loaded: stock argument-form parity (`-encoding name file`; any other leading `-` argument means no script, everything stays in `::argv`; no `-e` one-liner - deliberate stock parity, see G-077 for the punk-level one-liner direction), `lib:` refused with a pointer at `script`, missing script file is a clean exit 1. On piperepl-patched runtimes (the punk family runtimes - `src/buildsuites/suite_tcl90/patches/`) the C-level machinery publishes `::tclsh` launch state (`istty`, `dorepl`, `evalinput`, `inputbuffer`, `reopened`) allowing piped input to hand over to a live console repl at eof; runtimes without the machinery evaluate piped stdin whole-buffer with honest exit codes and fail fast on console stdin. The subcommand-family contract (tclsh/script/shell/punk/shellspy/buildinfo/help) is documented as punk::args definitions in `src/modules/punk/args/moduledoc/punkexe-999999.0a1.0.tm` (ids `(script)::punkexe` + per-subcommand; view via `punk::args::usage`), which the G-032 launcher help wiring renders and parses from at dispatch time (a help flag as tclsh's SOLE argument is the one deliberate stock-parity exception). Pipe-testable behaviour pinned by `src/tests/shell/testsuites/punkexe/tclshcmd.test` and `launcherhelp.test`.
- **`script` subcommand (G-015, achieved).** The lean non-interactive path: default punk shell module/alias environment, no shellfilter stacks/transforms, honest exit codes, launch plumbing emits nothing on stdout/stderr (exec-style callers see only the script's own output). Supports `lib:<name>` scriptlib resolution; a first argument of exactly `-e` runs the second argument as a Tcl one-liner (G-077, achieved - `::argv0` is `-e`, trailing args in `::argv`, a usage error when the script argument is missing, stdin never read by the form itself, non-empty final result echoed), also reachable as top-level `<punkexe> -e` via a boot-core reclassification ahead of the unknown-first-arg refusal. App package: `src/lib/app-punkscript/`.
- **`tclsh` subcommand (G-118, achieved).** Plain-tclsh emulation with NO punk modules loaded: stock argument-form parity (`-encoding name file`; any other leading `-` argument means no script, everything stays in `::argv`; no `-e` one-liner - deliberate stock parity; the punk-level one-liner lives at the top level and as `script -e` (G-077, achieved 2026-08-04 - goals/archive/G-077-punkexe-dash-e-oneliner.md)), `lib:` refused with a pointer at `script`, missing script file is a clean exit 1. On piperepl-patched runtimes (the punk family runtimes - `src/buildsuites/suite_tcl90/patches/`) the C-level machinery publishes `::tclsh` launch state (`istty`, `dorepl`, `evalinput`, `inputbuffer`, `reopened`) allowing piped input to hand over to a live console repl at eof; runtimes without the machinery evaluate piped stdin whole-buffer with honest exit codes and fail fast on console stdin. The subcommand-family contract (tclsh/script/shell/punk/shellspy/buildinfo/help) is documented as punk::args definitions in `src/modules/punk/args/moduledoc/punkexe-999999.0a1.0.tm` (ids `(script)::punkexe` + per-subcommand; view via `punk::args::usage`), which the G-032 launcher help wiring renders and parses from at dispatch time (a help flag as tclsh's SOLE argument is the one deliberate stock-parity exception). Pipe-testable behaviour pinned by `src/tests/shell/testsuites/punkexe/tclshcmd.test` and `launcherhelp.test`.
- **`bin/` launch surface.** Built kits plus polyglot `.cmd` utilities (`punk-runtime`, `punk-getzig`, `punk-tclargs`, `dtplite`) generated from `src/scriptapps/` sources by the `punk::mix` scriptwrap machinery - outputs are never hand-edited. `bin/punk-runtime.cmd` fetches/lists/selects plain runtimes under `bin/runtime/<platform>/` from the punkbin artifact server with sha1 verification. Source: `bin/AGENTS.md`.
## Interp and thread topology
@ -113,7 +113,7 @@ Mechanics here are documented as pointers only - detailed prose would be obsolet
- Input mode and completion: G-013 (raw-mode default), G-044 (punk::args-driven completion).
- Stored configuration and help: G-014 (punk::config toml), G-042 (subshell help topics), G-043 (subshell definition plugins).
- Console err channel: G-011.
- Launch surface: G-023 (version-named binaries), G-077 (`-e` one-liners), G-089 (scriptlib in kits).
- Launch surface: G-023 (version-named binaries), G-089 (scriptlib in kits).
- 8.6 buildsuite family: G-101 (kit container) - the core runtime (G-099) and the Tk/tklib/tcllib dependency set (G-100) are achieved 2026-07-26.
## Doc map - which lane holds what

19
CHANGELOG.md

@ -5,6 +5,25 @@ The latest `## [X.Y.Z]` header must match the `version` field in `punkproject.to
Entries are newest-first; one bullet per notable change. See the root `AGENTS.md`
"Project Versioning" section for the bump policy.
## [0.50.0] - 2026-08-04
- punk executable -e one-liner support (G-077, achieved): `<punkexe> -e
<script> ?args...?` and `<punkexe> script -e <script> ?args...?` run Tcl
code as a one-liner in the punk script environment - the perl/python/node
reflex now works on punk kits instead of erroring. `::argv0` is `-e`,
trailing args land in `::argv`, a non-empty final result is echoed (as
for the stdin form), errors print errorInfo to stderr with exit 1, and a
`-e` with no following script argument is a usage error - never an
interactive fall-through and never stock tclsh's -e misparse (argv-swallow
+ stdin read). The top-level form is a boot-core 0.4.0 reclassification
ahead of the G-032 unknown-first-arg refusal (script-default kits only;
tool-style kits keep `-e` routed to their processor, and `<punkexe> tclsh
-e ...` keeps deliberate stock parity per G-118). app-punkscript 1.1;
moduledoc punkexe 0.5.0 documents both forms; the unknown-first refusal
hint and the degraded plain help name them. Characterization:
scriptexec.test (-e section, 10 pins) + launcherhelp.test - 49/49 green
on the rebaked punk902z/punksys/punk86.
## [0.49.8] - 2026-08-04
- commandstack 0.7.0 (G-160 follow-on, landed at user direction): the

1
README.md

@ -107,6 +107,7 @@ For evaluating uncommitted source without minting or baking, a built executable
- `<punkexe> script` subcommand for reliable non-interactive script execution:
- `punkexe script <file.tcl> ?args?` — runs a script file with conventional `::argv0`/`::argv`
- `commands | punkexe script` — runs piped stdin commands and exits at EOF (no trailing `exit` needed)
- `<punkexe> -e <script> ?args?` — runs Tcl code as a one-liner (equivalently `punkexe script -e ...`): the perl/python/node `-e` reflex, with `::argv0` = `-e` and trailing args in `::argv`
- Honest exit codes (0 success, 1 error with errorInfo on stderr); no shellfilter transforms or logging side effects.
- `lib:<name>` resolution for scriptlib scripts: `punkexe script lib:hello` or bare `punkexe lib:hello`
- pluggable console backends: the REPL can run against non-detectable terminal-like devices via `::opunk::Console` subclasses (ssh-channel, tk-widget, test-double), with size, eof, and capability answered by subclass overrides.

2
bin/AGENTS.md

@ -97,7 +97,7 @@ platform-specific difference left in examples is path separator style
### Launch package modes (built punk shells)
The first argument to a built punk shell may be a dash-delimited package mode composed of tokens from `minted`, `os`, `src`, `internal` (e.g. `punksys src`, `punk902z minted-os`), optionally scoped with the `proj:` prefix (e.g. `punk91 proj:internal-src`). (`minted` was named `dev` before punkshell 0.43.0 - clean rename, no alias: old kits accept only `dev`, current kits only `minted`.) `internal` is the default and is always appended when absent. A first argument that is not a valid mode list is treated as a subcommand instead (a `proj:` prefix on a non-mode string is not consumed either). Implementation: `src/vfs/_config/punk_main.tcl` (search `all_package_modes`). The full packagemode contract (block meanings, ordering, proj: examples) is the punk::args documentation at id `(script)::punkexe` (module `punk::args::moduledoc::punkexe`), and the launch surface is self-documenting in every current kit (G-032): `<punkexe> help ?subcommand?`, `<punkexe> -help` or `<punkexe> <subcommand> -help` render tabled usage (with the packagemode contract as the leader row) - degrading to a plain subcommand list where the punk::args stack is unavailable. An unknown first argument that names no existing file (and no `lib:*` scriptlib reference) is refused with that usage rather than silently attempted as a script.
The first argument to a built punk shell may be a dash-delimited package mode composed of tokens from `minted`, `os`, `src`, `internal` (e.g. `punksys src`, `punk902z minted-os`), optionally scoped with the `proj:` prefix (e.g. `punk91 proj:internal-src`). (`minted` was named `dev` before punkshell 0.43.0 - clean rename, no alias: old kits accept only `dev`, current kits only `minted`.) `internal` is the default and is always appended when absent. A first argument that is not a valid mode list is treated as a subcommand instead (a `proj:` prefix on a non-mode string is not consumed either). Implementation: `src/vfs/_config/punk_main.tcl` (search `all_package_modes`). The full packagemode contract (block meanings, ordering, proj: examples) is the punk::args documentation at id `(script)::punkexe` (module `punk::args::moduledoc::punkexe`), and the launch surface is self-documenting in every current kit (G-032): `<punkexe> help ?subcommand?`, `<punkexe> -help` or `<punkexe> <subcommand> -help` render tabled usage (with the packagemode contract as the leader row) - degrading to a plain subcommand list where the punk::args stack is unavailable. An unknown first argument that names no existing file (and no `lib:*` scriptlib reference) is refused with that usage rather than silently attempted as a script. A first argument of exactly `-e` instead runs the following argument as a Tcl one-liner (G-077; equivalently `<punkexe> script -e <script> ?arg ...?`).
`src` mode is the one that matters for verifying working-tree changes:

2
punkproject.toml

@ -1,6 +1,6 @@
[project]
name = "punkshell"
version = "0.49.8"
version = "0.50.0"
license = "BSD-2-Clause"
url = "https://www.gitea1.intx.com.au/jn/punkshell"
#packager: declared identity for published artifacts (declarative, not proof -

2
src/lib/AGENTS.md

@ -16,7 +16,7 @@ Source of truth for all editable `pkgIndex.tcl`-based library packages. These ar
- Libraries here include:
- `app-punk/` — Punk REPL app entry (`app-punk::repl`)
- `app-punkshell/` — Punkshell app entry (`app-punkshell`); on eof of its input channel the `env(PUNK_PIPE_EOF)` policy (`exit`|`interactive`|unset=console heuristic) decides between terminating and reopening the console for an interactive repl — automated callers should set `exit` or end piped input with an explicit `exit`
- `app-punkscript/` — lean one-shot script runner behind the punk executable `script` subcommand (G-015): runs a file, a `lib:<name>` scriptlib script (resolved via `punk::path::scriptlib_resolve`; an extensionless call also matches an extensionless file whose first lines identify it as tcl - `# tcl` comment or tcl shebang incl. the sh-trampoline - with the called spelling winning when both spellings exist), or piped stdin in the default punk shell module/alias environment with honest exit codes; no shellfilter stacks or logging side effects, never falls into an interactive shell — the reliable path for automated/agent callers (prefer this over piping into `shell`); stdin form echoes the script's final result when non-empty; GUI parity with `tclsh` — a script leaving a registered Tk main loop is serviced until its window closes (so `script app.tcl` keeps a Tk app alive), while console scripts and errored scripts exit immediately
- `app-punkscript/` — lean one-shot script runner behind the punk executable `script` subcommand (G-015): runs a file, a `lib:<name>` scriptlib script (resolved via `punk::path::scriptlib_resolve`; an extensionless call also matches an extensionless file whose first lines identify it as tcl - `# tcl` comment or tcl shebang incl. the sh-trampoline - with the called spelling winning when both spellings exist), or piped stdin in the default punk shell module/alias environment with honest exit codes; no shellfilter stacks or logging side effects, never falls into an interactive shell — the reliable path for automated/agent callers (prefer this over piping into `shell`); stdin form echoes the script's final result when non-empty; GUI parity with `tclsh` — a script leaving a registered Tk main loop is serviced until its window closes (so `script app.tcl` keeps a Tk app alive), while console scripts and errored scripts exit immediately; G-077 (1.1): a first argument of exactly `-e` runs the second argument as a Tcl one-liner (`::argv0` is `-e`, trailing args in `::argv`, a usage error when the script argument is missing, stdin never read by the form itself, non-empty final result echoed as for the stdin form) — also reachable as top-level `<punkexe> -e` via a boot-core reclassification (core 0.4.0)
- `app-shellspy/` — ShellSpy app entry (`app-shellspy`)
- `app_shell/` — Shell app helpers (`app_shell`)
- `app_shellrun/` — Shell run helpers (`app_shellrun`)

2
src/lib/app-punkscript/pkgIndex.tcl

@ -1 +1 @@
package ifneeded app-punkscript 1.0 [list source [file join $dir punkscript.tcl]]
package ifneeded app-punkscript 1.1 [list source [file join $dir punkscript.tcl]]

39
src/lib/app-punkscript/punkscript.tcl

@ -1,9 +1,16 @@
package provide app-punkscript 1.0
package provide app-punkscript 1.1
#Lean one-shot script runner for the punk executable 'script' subcommand (goal G-015).
#
#Contract (G-015 - see goals/archive/G-015-script-subcommand-piped-stdin.md):
# - runs a script FILE (first argument, remaining arguments become the script's ::argv)
# or, with no arguments, the whole of piped/redirected stdin as the script.
# - G-077: a first argument of exactly '-e' runs the SECOND argument as a Tcl
# one-liner (remaining arguments become ::argv, ::argv0 is '-e') - the
# perl/python/node one-liner reflex in the punk script environment. Stock
# tclsh's -e misparse (argv-swallow + stdin read) is never reproduced: '-e'
# with no following script argument is a usage error, and this form never
# reads stdin itself (the one-liner may). A file literally named '-e' stays
# reachable as './-e'.
# - the script interp carries the default punk shell module/alias environment
# (package punk registers the deck aliases such as 'dev'; punk::aliascore
# provides the utility aliases) so one-liners like 'dev projects.work *x*'
@ -68,6 +75,36 @@ apply {{} {
catch {package require punk::args::moduledoc::tclcore}
set arglist $::argv
if {[llength $arglist] && [lindex $arglist 0] eq "-e"} {
#-e one-liner form (G-077): script -e <script> ?args...? - evaluate the
#given Tcl code with ::argv0 '-e' and the remaining arguments in ::argv.
#Works with or without piped stdin present: stdin is never read here,
#the one-liner may read it itself. '-e' with no following script
#argument is a usage error - never an interactive fall-through (and
#never stock tclsh's argv-swallow + stdin-read misparse).
if {[llength $arglist] < 2} {
puts stderr "punk script: -e requires a script argument"
puts stderr "usage: <punkexe> script -e <script> ?args...?"
puts stderr " or: <punkexe> -e <script> ?args...?"
exit 1
}
set onescript [lindex $arglist 1]
set ::argv0 "-e"
set ::argv [lrange $arglist 2 end]
set ::argc [llength $::argv]
set ::tcl_interactive 0
if {[catch {uplevel #0 $onescript} result]} {
puts stderr $::errorInfo
flush stderr
exit 1
}
#one-shot eval ergonomics, as for the stdin form: a non-empty final
#result is written to stdout (file-form scripts keep pure semantics).
if {[string length $result]} {
puts stdout $result
}
apply $finish 0
}
if {[llength $arglist]} {
#file form: scriptname ?args...?
#works with or without piped stdin present - the script may read stdin itself

36
src/modules/punk/args/moduledoc/punkexe-999999.0a1.0.tm

@ -106,7 +106,9 @@ tcl::namespace::eval punk::args::moduledoc::punkexe {
The next argument selects a subcommand from the choices below.
When the first non-mode argument is not a known subcommand:
with no arguments at all the interactive punk shell is
launched ('shell'); a first argument naming an existing file
launched ('shell'); a first argument of exactly '-e' runs the
following argument as a Tcl one-liner (handled as for
'script -e' - G-077); a first argument naming an existing file
(or a lib:* scriptlib reference) is treated as a script
invocation (handled as for the 'script' subcommand); anything
else is refused with this usage on stderr, exit 1 (G-032 -
@ -184,7 +186,8 @@ tcl::namespace::eval punk::args::moduledoc::punkexe {
#early link), item 6 (piped whole-buffer eval + honest exit codes),
#item 8 (istty read-only by convention). The 'no -e one-liner' note is
#the G-077 boundary: stock parity INSIDE this subcommand is deliberate;
#one-liner affordances belong to the top-level/'script' surfaces (G-077).
#the one-liner affordance it points at ('-e' at the top level and as
#the script subcommand's first argument) landed with G-077 (0.5.0).
# -- --- --- --- --- --- --- --- --- --- --- --- --- ---
lappend PUNKARGS [list {
@id -id (script)::punkexe::tclsh
@ -205,8 +208,10 @@ tcl::namespace::eval punk::args::moduledoc::punkexe {
input comes from stdin. In particular there is no -e/-c
one-liner flag - '<punkexe> tclsh -e (script)' puts '-e' and the
script text into ::argv and reads stdin, exactly like stock
tclsh. That parity is deliberate; one-liner affordances belong
to the punk-level launch surfaces, not this subcommand.
tclsh. That parity is deliberate; the one-liner affordance
lives on the punk-level launch surfaces instead (G-077): use
'<punkexe> -e <script> ?arg ...?' or '<punkexe> script -e
<script> ?arg ...?', never this subcommand.
G-032 help carve-out (the single deliberate exception): a help
flag (-help/--help/-h//?) as the SOLE argument renders this
usage and exits 0; the same flag accompanied by ANY further
@ -318,16 +323,37 @@ tcl::namespace::eval punk::args::moduledoc::punkexe {
from the scriptlib locations associated with the executable,
including extensionless and shebang'd scripts).
A first argument of exactly '-e' runs the SECOND argument as a
Tcl one-liner (G-077): non-interactive, ::argv0 is '-e',
arguments after the script text land in ::argv, and a
non-empty final result is echoed to stdout (the same one-shot
eval ergonomics as the stdin form). '-e' with no following
script argument is a usage error - never an interactive
fall-through, and never stock tclsh's -e misparse (argv-swallow
plus stdin read). The form works with or without piped stdin
present; stdin is read only if the one-liner itself reads it.
A file literally named '-e' stays reachable as './-e'. The
same one-liner is available at the top level as
'<punkexe> -e <script> ?arg ...?'.
A help flag (-help/--help/-h//?) as the FIRST argument renders
this usage and exits 0 (G-032); in any later position it is an
ordinary script argument."
@form -synopsis "<punkexe> script ?scriptfile|lib:name? ?arg ...?"
@form -form {file} -synopsis "<punkexe> script ?scriptfile|lib:name? ?arg ...?"
@values -min 0 -max -1
scriptfile -type string -optional 1 -help -&
"Script file path, or lib:name for scriptlib resolution.
Omitted: the script is read from piped stdin."
arg -type any -optional 1 -multiple 1 -help -&
"Arguments passed to the script in ::argv."
@form -form {oneliner} -synopsis "<punkexe> script -e <script> ?arg ...?"
@values -min 1 -max -1
script -type string -optional 0 -help -&
"Tcl code to evaluate as a one-liner (G-077): errors print
errorInfo to stderr with exit 1; a non-empty final result
is echoed to stdout."
arg -type any -optional 1 -multiple 1 -help -&
"Arguments passed to the one-liner in ::argv (::argv0 is '-e')."
}]
# -- --- --- --- --- --- --- --- --- --- --- --- --- ---
lappend PUNKARGS [list {

3
src/modules/punk/args/moduledoc/punkexe-buildversion.txt

@ -1,6 +1,7 @@
0.4.0
0.5.0
#First line must be a semantic version number
#all other lines are ignored.
#0.5.0 - G-077 -e one-liner forms documented: (script)::punkexe::script gains the oneliner @form + -help paragraph; top-level (script)::punkexe documents the '-e' first-argument reclassification and gains its oneliner @form; (script)::punkexe::tclsh boundary text now points at the live forms
#0.4.0 - G-032 launcher help/parse wiring live: help subcommand documented ((script)::punkexe::help + choices entry); top-level unknown-first-arg refusal rule + help discovery documented; per-subcommand first-arg -help notes (tclsh sole-arg carve-out); buildinfo argument-refusal shape; passthrough-vs-parsed decision recorded
#0.3.0 - buildinfo subcommand documented (G-025): (script)::punkexe::buildinfo definition + choices entry; stamp/live/source distinction + in-shell equivalent noted
#0.2.0 - package_mode 'dev' renamed 'minted' (clean break, no alias - stage-true vocabulary follow-on to G-155): mode docs updated; the built-output mode now names what it consumes (mint output trees)

2
src/tests/shell/AGENTS.md

@ -21,7 +21,7 @@ Tests for shell-level behavior, command-line execution, and stdin/stdout interac
- `testsuites/punkexe/maketcllibfetch.test` (G-139 follow-up) characterizes `make.tcl libfetch` through the same piped `script` harness against FIXTURE origins only - a `testsupport/httpfixture.tcl` server plus a `file://` mirror of a crafted punkbin `lib/<target>` layout; the real canonical origin is never contacted. Pins: the server-trust consent gate (any non-canonical origin - `PUNKBIN_URL` env or `-serverurl` - without `-trust-server` refuses with exit 3 BEFORE any network access or tier write, naming both the offending and canonical origins), `-serverurl`-beats-`PUNKBIN_URL` precedence, consented fetch/verify/materialize (per-target sha1sums + zip + sidecar into the tier, installed-shape tree with its embedded `punkbin-artifact.toml`), idempotent re-run vs `-force`, declared-revision-change re-materialization keyed to the tree's embedded record (including the sidecar-not-listed note lane), sha1-MISMATCH rejection without residue, artifact-missing-from-server-sha1sums failure, and the no-config no-op. Isolation rides the env seams `PUNK_LIBFETCH_CONFIG` + `PUNK_LIBFETCH_PACKAGES` (all state in tcltest tempdirs - the real `src/runtime/libpackages.toml` and `bin/packages` tier are never touched; the seams deliberately cannot bypass the consent gate). The asserted canonical-origin url is characterization of the constant in `src/make.tcl` - a deliberate canonical-origin change legitimately updates it.
- `testsuites/punkexe/shellexit.test` guards piped-stdin termination behaviour of the built executable (no `invalid thread handle` on shutdown races, no console-reopen hang on eof/error paths, exitcode propagation); do not weaken these tests - a red result means a regression in punk::repl shutdown or app-punkshell eof handling.
- `testsuites/punkexe/shellnavns.test` proves the namespace-navigation session contract end-to-end over a piped 'src'-mode session (dev modules - deliberate, so current punk::repl/nav source is exercised without a rebuild): n/ sets ns_current, the current namespace continues into `subshell punk` and back out (currently via the documented first-subshell shared-code-interp asymmetry - the SHARED marker pins it; the repl::start seeding template for genuinely separate interps is proven in modules/punk/repl nscurrent.test), the punk::repl 0.5.2 stray-namespace fix holds live, and the session exits cleanly. KNOWN GAP pinned there: piped submissions evaluate at :: despite ns_current (interactive and module-level inscope behaviour differ) - investigate alongside G-038.
- `testsuites/punkexe/scriptexec.test` covers the `script` subcommand (app-punkscript, G-015): piped execution + honest exit codes, result echo, file-form argv, the `lib:<name>` scriptlib-resolution mechanism (incl. punk::path 0.3.0 extensionless-tcl matching: first-line identification, called-spelling precedence over a `.tcl` sibling, sh-trampoline shebang acceptance, and the non-tcl skip note on not-found), and the tclsh-matching Tk main-loop behaviour. The `lib:` and Tk cases resolve committed fixtures under `scriptlib/_punktest/` (test-owned; a `lib:`-resolvable fixture cannot live in a tcltest temp dir since resolution only searches scriptlib locations relative to the exe). Tk cases are gated by the `punktk` constraint, auto-detected by probing whether the kit can load Tk.
- `testsuites/punkexe/scriptexec.test` covers the `script` subcommand (app-punkscript, G-015): piped execution + honest exit codes, result echo, file-form argv, the `lib:<name>` scriptlib-resolution mechanism (incl. punk::path 0.3.0 extensionless-tcl matching: first-line identification, called-spelling precedence over a `.tcl` sibling, sh-trampoline shebang acceptance, and the non-tcl skip note on not-found), the `-e` one-liner form (G-077: top-level and `script -e` surfaces, `::argv0`/`::argv` setup, piped-stdin read and unread, the no-script-argument usage error on both surfaces, errorInfo + exit 1, result echo, and the `tclsh -e` stock-parity boundary pin), and the tclsh-matching Tk main-loop behaviour. The `lib:` and Tk cases resolve committed fixtures under `scriptlib/_punktest/` (test-owned; a `lib:`-resolvable fixture cannot live in a tcltest temp dir since resolution only searches scriptlib locations relative to the exe). Tk cases are gated by the `punktk` constraint, auto-detected by probing whether the kit can load Tk.
- punkexe tests run against the built binary: after changing punk::repl or app-punkshell source, rebuild via `make.tcl packages`, `make.tcl vfscommonupdate -confirm 0` (unattended runs must pass the flag - the REPLACE confirmation aborts fast on non-interactive stdin instead of reading a piped `y`), then `make.tcl bake` before trusting results; `make.tcl bake`/`bakehouse` alone does not refresh `_vfscommon.vfs` (vfscommonupdate is the promotion gate).
- `testsuites/punkexe/tclshcmd.test` covers the `tclsh` subcommand (punk_main.tcl dispatch): script-file sourcing (argv0/argv/info-script state, honest exit codes incl. explicit exit), piped no-arg stdin evaluation, stock argument forms (leading `-` arg = no script, all args stay in `::argv`; `-encoding name file` sources with the named encoding, incomplete `-encoding` forms fall through to argv; verified byte-level via a utf-8/iso8859-1 fixture), the `lib:` refusal (plain-tclsh semantics - pointer to the `script` subcommand, exit 1), and - on kits built on a TCLSH_PIPEREPL-patched runtime (G-096/G-103, see `src/buildsuites/suite_tcl90/patches/README.md`) - the piperepl launch-state contract: ::tclsh(istty) published, piped script-arg launch state all-zero, unconsumed piped input NOT evaluated unless the script sets `::tclsh(evalinput) 1`, and the script's right to consume stdin itself; plus the degraded-mode notice on unpatched runtimes. Patched-kit resolution: `env(PUNK_PIPEREPL_TEST_EXE)` if it probes as patched, else the punkexe if patched, else `bin/punk9_beta.exe`/`bin/punk9bi_beta.exe`. HANG RULE: no PIPED test or fixture may set `::tclsh(dorepl)` - that path reopens stdin from the console (CONIN$) and blocks under the piped punk_run harness. Console-reopen coverage exists but is env-gated (G-118 item 11; constraint `punkconsole` = windows + env `PUNK_TEST_CONSOLE=1`, normal runs skip): two tests drive the dorepl path end-to-end in the child's OWN hidden console per the G-106 recipe - an outer `Start-Process -WindowStyle Hidden` powershell wrapper owns a fresh console and runs `<kit> tclsh` there with std handles redirected to files, then `testsupport/consoleinject.ps1` attaches to that console and types the verification lines + `exit 0` as key events. They pin: `::tclsh(reopened)` flips 0 -> 1 at reopen while `istty` stays 0 (immutable launch fact), `::tclsh(inputbuffer)` appears empty (the repl loop consumed all piped input), the reopen notice is silent by default and appears on STDERR only under `TCLSH_PIPEREPL_DEBUG=1`, and the injected exit completes the process (exit code 0). Deadlines plus wrapper-tree kill keep them hang-proof.
- `testsuites/binscripts/runtimebash_wsl.test` (G-059) exercises the punk-runtime.cmd unix payload (`src/scriptapps/bin/punk-runtime.bash`) through WSL: active/use/run resolution, env override, stale-active guidance, single-candidate fallback, and `list -remote` local-vs-server comparison (offline via a crafted cached sha1sums.txt + `PUNKBIN_URL` pointed at an unreachable address). Gated by the `wsllinux` capability constraint from `src/tests/testsupport/wslprobe.tcl`; all execution happens in a WSL-native staging dir, and a final test asserts the Windows checkout's git status is unchanged by the run.

4
src/tests/shell/testsuites/punkexe/launcherhelp.test

@ -169,9 +169,11 @@ namespace eval ::testspace {
lappend result [expr {[dict get $r stdout] eq ""}]
lappend result [string match "*unknown subcommand*" [dict get $r stderr]]
lappend result [string match "*script nosuchthing_g032*" [dict get $r stderr]]
#G-077: the refusal hint also points at the -e one-liner forms
lappend result [string match "*for a one-liner use*script -e*" [dict get $r stderr]]
set result
}\
-result {1 1 1 1}
-result {1 1 1 1 1}
test launcherhelp_unknownfirst_file_runs {unknown first arg naming an existing file still runs as script}\
-constraints {punkexeavailable kit_g032} -body {

104
src/tests/shell/testsuites/punkexe/scriptexec.test

@ -4,7 +4,10 @@ package require tcltest
#Covers: piped-stdin execution and honest exit codes, result echo, file-form argv passing,
#the lib:<name> scriptlib-resolution mechanism (incl. subpath, not-found, and punk::path
#0.3.0 extensionless-tcl matching: first-line identification, called-spelling precedence,
#sh-trampoline shebang, non-tcl skip note), and the tclsh-matching Tk main-loop behaviour
#sh-trampoline shebang, non-tcl skip note), the -e one-liner form (G-077: top-level and
#'script -e', ::argv0/::argv setup, piped-stdin interaction, usage error on a missing
#script argument, errorInfo+exit 1 on script error, result echo, and the 'tclsh -e'
#stock-parity boundary), and the tclsh-matching Tk main-loop behaviour
#(a GUI script's event loop is serviced after the script body - punkshell 0.4.1).
#
#The lib: and Tk cases resolve committed fixtures under scriptlib/_punktest/ (test-owned;
@ -327,6 +330,105 @@ namespace eval ::testspace {
lappend result [dict get $rd exitcode]
} -result [list 0 1 1 0 1]
# -- -e one-liner form (G-077) ------------------------------------------------
#added 2026-08-04 (agent, G-077)
test script_dashe_simple {'script -e {puts hi}' runs the one-liner and exits 0}\
-constraints punkexeavailable -setup $common -body {
variable punkexe
set rd [punk_run $punkexe [list script -e "puts G077_MARKER"] ""]
lappend result [dict get $rd timedout]
lappend result [expr {[string first "G077_MARKER" [dict get $rd output]] >= 0}]
lappend result [dict get $rd exitcode]
} -result [list 0 1 0]
test script_dashe_toplevel {top-level '-e' (no subcommand word) reclassifies to the script -e form}\
-constraints punkexeavailable -setup $common -body {
variable punkexe
set rd [punk_run $punkexe [list -e "puts G077_TOP_MARKER"] ""]
lappend result [dict get $rd timedout]
lappend result [expr {[string first "G077_TOP_MARKER" [dict get $rd output]] >= 0}]
lappend result [dict get $rd exitcode]
} -result [list 0 1 0]
test script_dashe_argv {arguments after the one-liner land in ::argv; ::argv0 is '-e'}\
-constraints punkexeavailable -setup $common -body {
variable punkexe
set rd [punk_run $punkexe [list script -e {puts [list G077_ARGV0=$::argv0 G077_ARGV=$::argv]} alpha beta] ""]
lappend result [dict get $rd timedout]
lappend result [expr {[string first "G077_ARGV0=-e" [dict get $rd output]] >= 0}]
lappend result [expr {[string first "G077_ARGV=alpha beta" [dict get $rd output]] >= 0}]
lappend result [dict get $rd exitcode]
} -result [list 0 1 1 0]
test script_dashe_stdin_unread {piped stdin present but unread: no swallow, no hang, exit 0}\
-constraints punkexeavailable -setup $common -body {
variable punkexe
set rd [punk_run $punkexe [list script -e "puts G077_NOREAD"] "this stdin is never read\n"]
lappend result [dict get $rd timedout]
lappend result [expr {[string first "G077_NOREAD" [dict get $rd output]] >= 0}]
lappend result [dict get $rd exitcode]
} -result [list 0 1 0]
test script_dashe_reads_stdin {the one-liner may read piped stdin itself}\
-constraints punkexeavailable -setup $common -body {
variable punkexe
set rd [punk_run $punkexe [list script -e {puts [string toupper [read stdin]]}] "g077 from stdin\n"]
lappend result [dict get $rd timedout]
lappend result [expr {[string first "G077 FROM STDIN" [dict get $rd output]] >= 0}]
lappend result [dict get $rd exitcode]
} -result [list 0 1 0]
test script_dashe_result_echo {a non-empty final result is echoed (stdin-form one-shot ergonomics)}\
-constraints punkexeavailable -setup $common -body {
variable punkexe
set rd [punk_run $punkexe [list script -e {expr {6 * 7}}] ""]
lappend result [dict get $rd timedout]
lappend result [expr {[string first "42" [dict get $rd output]] >= 0}]
lappend result [dict get $rd exitcode]
} -result [list 0 1 0]
test script_dashe_error {error in the one-liner: errorInfo on stderr, exit 1, no hang}\
-constraints punkexeavailable -setup $common -body {
variable punkexe
set rd [punk_run $punkexe [list script -e {error G077_BOOM}] ""]
lappend result [dict get $rd timedout]
lappend result [expr {[string first "G077_BOOM" [dict get $rd output]] >= 0}]
lappend result [dict get $rd exitcode]
} -result [list 0 1 1]
test script_dashe_noarg {'script -e' with no script argument: usage error, exit 1, never interactive}\
-constraints punkexeavailable -setup $common -body {
variable punkexe
set rd [punk_run $punkexe [list script -e] ""]
lappend result [dict get $rd timedout]
lappend result [expr {[string first "-e requires a script argument" [dict get $rd output]] >= 0}]
lappend result [dict get $rd exitcode]
} -result [list 0 1 1]
test script_dashe_noarg_toplevel {top-level '-e' with no script argument: the same usage error, exit 1}\
-constraints punkexeavailable -setup $common -body {
variable punkexe
set rd [punk_run $punkexe [list -e] ""]
lappend result [dict get $rd timedout]
lappend result [expr {[string first "-e requires a script argument" [dict get $rd output]] >= 0}]
lappend result [dict get $rd exitcode]
} -result [list 0 1 1]
test script_dashe_tclsh_boundary {'tclsh -e' keeps stock parity: stdin is evaluated, '-e' stays in ::argv}\
-constraints punkexeavailable -setup $common -body {
variable punkexe
#G-077/G-118 boundary: the tclsh subcommand deliberately never grows
#the one-liner - stock's -e misparse (argv-swallow + stdin read) is
#its contract, so the piped stdin text is what runs.
set rd [punk_run $punkexe [list tclsh -e {puts G077_SHOULD_NOT_PRINT}] "puts G077_TCLSH_STDIN\n"]
lappend result [dict get $rd timedout]
lappend result [expr {[string first "G077_TCLSH_STDIN" [dict get $rd output]] >= 0}]
lappend result [expr {[string first "G077_SHOULD_NOT_PRINT" [dict get $rd output]] < 0}]
lappend result [dict get $rd exitcode]
} -result [list 0 1 1 0]
# -- Tk main-loop behaviour (constraint punktk) ------------------------------------------------
test script_tk_countdown_serviced {GUI script's event loop is serviced: after-callback runs and exits 0}\

1
src/vfs/AGENTS.md

@ -18,6 +18,7 @@ VFS (Virtual File System) folders define the runtime payloads that get wrapped i
- A `*.vfs` folder baked against a runtime is expected to carry a root-level startup script: an actual `main.tcl`, or a root fauxlink resolving to the name `main.tcl` whose target exists and is a `.tcl` file. `make.tcl` warns when neither is present (column-0 `BAKE-WARNING:` token, ANSI-highlighted, recapped at end of run like provenance warnings) but still bakes - a kit without a startup script is legal. It must NOT carry both, nor two root fauxlinks resolving to `main.tcl`: the bake REFUSES such a kit per-kit under FAILED KITS (G-031 collision gate, predicate `punkboot::utils::vfs_startup_script_report` - merge traversal order would otherwise silently decide the boot script). Overlay-only folders with no runtime mapping (e.g `_vfscommon.vfs`) are not checked.
- Componentized boot (G-031, 0.45.0): the kit main is a THIN project-owned file that declares project subcommands (`::punkboot::project_subcommands` - built-ins tclsh/shellspy/punk/shell/script/buildinfo/help cannot be shadowed) and launch defaults (`noargs`/`unknownfirst`), publishes `::punkboot::launch_args`/`main_script`, then sources the layout-owned boot core: `<vfsroot>/punkboot/core.tcl` in kits (regenerated into `_vfscommon.vfs` from the `_config/punkboot_core.tcl` master by `make.tcl vfscommonupdate`), parent-dir candidate for nested `app/main.tcl`, sibling `punkboot_core.tcl` when a main runs as a plain script from `_config` (the kitmountpoint characterization shape). The core carries the boot machinery and default dispatch and is versioned (`::punkboot::core_version`). Behavioural statements about `punk_main.tcl`/`project_main.tcl` in the bullets below describe machinery that now lives in the boot core they source.
- Launcher help + declared parse (G-032, core 0.3.0): the dispatch renders and parses through punk::args - `<punkexe> help ?subcommand?` / `-help` / `<subcommand> -help` render tabled usage from the `punk::args::moduledoc::punkexe` ids plus the live registrations (per-kit clones `(script)::punkexe.launcher`/`.launcherhelp`); an unknown first argument naming no existing file (and no `lib:*` reference) is refused with usage (stderr, exit 1) instead of silently reclassifying to `script`; `buildinfo` enforces its no-arguments contract through punk::args (historic one-liner + tabled usage, exit 2). Optional per-name registration metadata `::punkboot::project_subcommand_info` (`summary`/`argsid`/`package`/`parse` - documented by `punk::args::moduledoc::punkboot`) puts project subcommands in the help tables by registration alone and can gate their arguments through a declared definition (usage errors exit 1, handler not invoked). Carve-outs: tool-style kits (`unknownfirst` naming a project subcommand) keep help FLAGS routed to their processor ('help' word remains); `tclsh` intercepts a help flag only as its SOLE argument (stock dash parity); a registration without `argsid` keeps `-help` passthrough to its handler. Everything is lazy + guarded (G-030 doctrine): normal boots never load punk::args, and with the stack unavailable (or `PUNKBOOT_PLAIN=1`) help degrades to a plain subcommand list while parsing degrades to the historic switch semantics. Characterization: `src/tests/shell/testsuites/punkexe/launcherhelp.test`.
- Top-level `-e` one-liner (G-077, core 0.4.0): on kits whose unknownfirst default is `script`, a first argument of exactly `-e` is reclassified to the `script` subcommand's own `-e` handling ahead of the G-032 unknown-first-arg refusal (the whole arglist, `-e` included, becomes its arguments) - `<punkexe> -e <script> ?arg ...?` runs the code as a one-liner (app-punkscript 1.1: `::argv0` is `-e`, trailing args in `::argv`, errorInfo + exit 1 on error, a usage error when the script argument is missing, stdin never read by the form itself, non-empty final result echoed). Tool-style kits keep `-e` routed to their processor; `<punkexe> tclsh -e ...` keeps deliberate stock parity (G-118). Characterization: `src/tests/shell/testsuites/punkexe/scriptexec.test` (-e section).
- VFS content must be compatible with the target platform runtime.
- A root-level `punkshell.ico` in a kit's own custom `.vfs` folder overrides
the embedded kit icon and the sidecar-recorded icon choice for that kit

46
src/vfs/_config/punkboot_core.tcl

@ -11,7 +11,8 @@
#package_mode parsing (minted|os|internal|src with the proj: scope prefix,
#G-033), module-path and auto_path assembly, punk::libunknown, src-mode
##modpod registration, then the default subcommand dispatch
#(tclsh/shellspy/punk/shell/script/buildinfo/help) extended by the thin
#(tclsh/shellspy/punk/shell/script/buildinfo/help plus the G-077 top-level
#'-e' one-liner reclassification to 'script -e') extended by the thin
#main's declared project subcommands (G-031 registration model - see the
#dispatch section near the end of this file). The launch surface documents
#itself via punk::args (G-032): '<punkexe> help ?subcommand?', '-help' and
@ -51,7 +52,7 @@
namespace eval ::punkboot {
#boot-core identity (G-031; stamped into kits + reported by the G-025
#buildinfo surfaces)
variable core_version 0.3.0
variable core_version 0.4.0
}
if {![info exists ::punkboot::launch_args]} {
namespace eval ::punkboot [list variable launch_args $::argv]
@ -1703,6 +1704,7 @@ if {![info exists ::punkboot::launch_defaults]} {
set exebase [file rootname [file tail [info nameofexecutable]]]
set lines [list]
lappend lines "Usage: $exebase ?packagemode? ?subcommand? ?arg ...?"
lappend lines " $exebase ?packagemode? -e <script> ?arg ...? (one-liner - same as 'script -e', G-077)"
lappend lines " packagemode: ordered dash-separated list of internal|minted|os|src (default internal),"
lappend lines " optionally scoped with the 'proj:' prefix (e.g proj:internal-src)"
lappend lines " subcommands:"
@ -1793,6 +1795,7 @@ if {![info exists ::punkboot::launch_defaults]} {
puts stderr "known subcommands: [join [launcher_plain_choices] {, }]"
}
puts stderr "use '$exebase help' for the launch surface, or '$exebase script $word ?arg ...?' to force script interpretation"
puts stderr "for a one-liner use '$exebase -e <script> ?arg ...?' (equivalently '$exebase script -e <script> ?arg ...?')"
return 1
}
}
@ -1872,19 +1875,32 @@ if {![info exists ::punkboot::launch_defaults]} {
::punkboot::launcher_show_help
exit 0
}
set subcommand $default_unknownfirst
if {$subcommand eq "script"} {
#G-032 unknown-first-arg reclassification rule (recorded in
#goals/G-032-launcher-punkargs.md): reclassify to 'script'
#only when the argument plausibly names a script - an
#existing file path, or a lib:* scriptlib reference. Anything
#else is refused with usage on stderr (exit 1); when
#punk::args is unavailable the refusal degrades to the
#historic always-reclassify behaviour.
set unknown_word [lindex $subcommand_arglist 0]
if {!([file exists $unknown_word] || [string match -nocase lib:* $unknown_word])} {
if {[::punkboot::launcher_unknown_first_error $unknown_word]} {
exit 1
#G-077: a first argument of exactly '-e' is the one-liner form -
#reclassify to 'script' with the WHOLE arglist ('-e' included) as
#its arguments, so the script subcommand's own '-e' handling runs
#it. This sits ahead of the unknown-first-arg refusal below, which
#would otherwise reject '-e' as naming no existing file. Same
#carve-out shape as the help flags: only script-default kits
#intercept it - a tool-style kit keeps '-e' routed to its own
#processor, and '<punkexe> tclsh -e ...' keeps stock parity
#(the tclsh subcommand never reaches this path).
if {$subcommand eq "-e" && $default_unknownfirst eq "script"} {
set subcommand script
} else {
set subcommand $default_unknownfirst
if {$subcommand eq "script"} {
#G-032 unknown-first-arg reclassification rule (recorded in
#goals/G-032-launcher-punkargs.md): reclassify to 'script'
#only when the argument plausibly names a script - an
#existing file path, or a lib:* scriptlib reference. Anything
#else is refused with usage on stderr (exit 1); when
#punk::args is unavailable the refusal degrades to the
#historic always-reclassify behaviour.
set unknown_word [lindex $subcommand_arglist 0]
if {!([file exists $unknown_word] || [string match -nocase lib:* $unknown_word])} {
if {[::punkboot::launcher_unknown_first_error $unknown_word]} {
exit 1
}
}
}
}

Loading…
Cancel
Save