25 KiB
ARCHITECTURE.md - Punkshell System Mechanics
About this document
This file is the descriptive, current-state map of how punkshell fits together. It occupies its own lane alongside the project's other documentation:
AGENTS.mdfiles (root + children) are binding work contracts: what you must do when working here.GOALS.md,GOALS-archive.mdandgoals/track intended and landed changes with rationale: what changed and why.- This file says how the system works now.
Rules that keep it useful instead of rotten:
- Pointer-first. Sections state durable mechanics briefly and route to canonical sources (module files, owning AGENTS.md, goal detail files). Never transcribe content that lives elsewhere - point at it. If a section wants to grow past a screenful, the detail belongs in the module or a child doc, with this section pointing at it.
- Current state only. History, rejected alternatives and verification records stay in goal detail files; release-level history stays in
CHANGELOG.md. Do not narrate change history here. - Staleness. When this file conflicts with the code, the code wins and this file is stale. Update or delete the stale section in the same change that breaks it (root
AGENTS.mdCloseout). Delete stale sections rather than annotating them. - In-flux areas. Subsystems under active redesign are documented as one-liners with goal pointers, not detailed prose (see "In-flux areas").
- Verification.
tclsh scriptlib/developer/architecture_lint.tcl- backtick-quoted repo paths must exist,G-<id>references must resolve against the goal indexes, size budget enforced. Run after editing this file.
The ten-thousand-foot view
Punkshell is an alternative Tcl shell distributed as self-contained executables ("kits"): a zipfs/metakit-aware Tcl runtime wrapped with a virtual filesystem (VFS) payload carrying the punk module set. A kit boots through a single entry script, which selects a package mode (which module sources to trust) and a subcommand (which application to run: interactive shell, non-interactive script runner, stock tclsh emulation, and others). The interactive shell is a REPL in the main interp that evaluates user code in a separate code thread and talks to the terminal through a two-layer console abstraction. Everything is built, tested and released by a Tcl-driven build system (src/make.tcl) with file-level provenance tracking (punkcheck).
bin/<kit>.exe = Tcl runtime (zipfs/metakit) + <name>.vfs payload
| boots
v
src/vfs/_config/punk_main.tcl
- package mode: dev / os / src / internal (which module sources to trust)
- subcommand: shell / script / punk / tclsh / shellspy
|
v
app package from src/lib/ (app-punkshell, app-punkscript, app-repl, app-shellspy)
|
v
+------------------- interactive shell --------------------+
| REPL interp (punk::repl) <--> console abstraction |
| + codethread (user code) (punk::console over |
| + shellthread workers opunk::Console) |
+----------------------------------------------------------+
Boot and launch chain
- Kit anatomy. Each
src/vfs/*.vfsfolder is a runtime payload;src/runtime/mapvfs.configmaps payload folders to platform runtimes underbin/runtime/<platform>/;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. An optional first argument is a dash-delimited package mode composed of tokens fromdev,os,src,internal(internalis always appended when absent), optionally scoped with theproj:prefix (G-033:dev/srcresolve 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. Any other first argument is treated as a script invocation; no arguments at all means interactive shell. Subcommands map to app packages undersrc/lib/(app-punkshell,app-punkscript,app-repl,app-shellspy). - Package modes pick module provenance.
internaluses module snapshots baked into the kit at build time;devloads build outputs from repo-rootmodules/andlib/;srcloads the unbuilt working tree (src/modules,src/lib,src/bootsupport,src/vendormodules) withpackage prefer latestso magic-version dev modules (999999.0a1.0) beat stamped snapshots.srcmode 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::mountand 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 assumingtcl::zipfs::root+//zipfs:/app. Modern runtimes still answer//zipfs:/app; the androwish/undroidwish 8.6 backport has norootcommand, mounts at the executable's own path, and boots the same payload through itsapp/main.tclin-archive hook (src/vfs/punk8win.vfscarries 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, andpunk::packagepreferenceresolves static-vs-bundled version-aware. Sources:src/vfs/AGENTS.md,goals/archive/G-058-static-runtime-packages.md. scriptsubcommand (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). Supportslib:<name>scriptlib resolution. App package:src/lib/app-punkscript/.tclshsubcommand (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-eone-liner - deliberate stock parity, see G-077 for the punk-level one-liner direction),lib:refused with a pointer atscript, 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::tclshlaunch 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) is documented as punk::args definitions insrc/modules/punk/args/moduledoc/punkexe-999999.0a1.0.tm(ids(script)::punkexe+ per-subcommand; view viapunk::args::usage), consumable by the future launcher help wiring (G-032). Pipe-testable behaviour pinned bysrc/tests/shell/testsuites/punkexe/tclshcmd.test.bin/launch surface. Built kits plus polyglot.cmdutilities (punk-runtime,punk-getzig,punk-tclargs,dtplite) generated fromsrc/scriptapps/sources by thepunk::mixscriptwrap machinery - outputs are never hand-edited.bin/punk-runtime.cmdfetches/lists/selects plain runtimes underbin/runtime/<platform>/from the punkbin artifact server with sha1 verification. Source:bin/AGENTS.md.
Interp and thread topology
- REPL interp vs codethread.
punk::replruns the reader/prompt loop in the main interp; user commands evaluate in a separate thread's interp (src/modules/punk/repl/codethread-999999.0a1.0.tm) so the reader stays responsive and user-code state is isolated from shell state. - shellfilter stacks. Channel transform/logging layers. Among other roles,
varJUNCTION stacks divert the code interp's stdout/stderr into per-run variables that the repl collects and emits to the console after each run (G-001). The API is known-clumsy; audit is proposed as G-090. Source:src/modules/shellfilter-999999.0a1.0.tm. - shellthread workers. Pooled worker threads (logging, tee handlers). tcludp 1.0.12's Windows per-thread exit handler closed process-global events and wedged worker event loops (G-036, achieved); kits now bundle tcludp >= 1.0.13 and a runtime
has_bug-style check surfaces the vulnerable combination. Worker lifecycle audit is also G-090. Source:src/modules/shellthread-999999.0a1.0.tm. - Shared state. Infrastructure
tsvarrays arepunk_-prefixed by contract (legacy unprefixed arrays predate the rule); console facts, ownership and mode caches live in tsv so every thread sees one truth (G-007). Source:src/modules/AGENTS.md. - Subshells. Named secondary repls exist; the substantial subshell roadmap is in-flux - see "In-flux areas".
Process exit and shutdown
- App-level ordered teardown. The shell does not leave thread/channel cleanup to Tcl's process finalization: the app package runs an explicit shutdown sequence while interps are fully alive -
shellthread::manager::shutdown_free_threadsterminates free workers under a bounded timeout (a wedged worker must not hang shutdown), remaining threads are released, the repl cancels its deferred stdin reader registration and drops input arriving after codethread teardown, and the codethread flushes stdout/stderr before returning results. Sources:src/lib/app-punkshell/punkshell.tcl,src/modules/shellthread-999999.0a1.0.tm,src/modules/punk/repl-999999.0a1.0.tm,src/tests/shell/testsuites/punkexe/shellexit.test. - Piped-stdin exit policy. PUNK_PIPE_EOF selects terminate vs console-reopen at piped EOF (G-015, achieved) so scripted termination is deterministic rather than dependent on console-availability heuristics. Source:
src/lib/app-punkshell/punkshell.tcl. - Upstream constraint: Tcl finalization order is not a documented contract. Tcl_Finalize runs a fixed-but-undocumented subsystem order in which extension exit handlers can fire while other threads are still alive and while channels, allocators and mutexes are mid-teardown - the shape behind the G-036 tcludp worker wedge ("Interp and thread topology") and the dead-console class ("Console abstraction"). Current upstream state: trunk carries a partial fix for the busy-thread finalize crash (tcl ticket e55a589d2b, 2026-05) explicitly NOT backported to 9.0, so 9.0.x runtimes (buildsuite target 9.0.5) still carry it. Work guidance derived from this constraint:
src/AGENTS.md"Work Guidance". Surveyed core-source detail:TEMP_REFERENCE/AGENTS.mdsurveyed-areas entry fortcl9/finalize path.
Console abstraction
Two layers with a deliberate dependency direction (the class never depends on the integrating layer):
opunk::Console(src/modules/opunk/) - avoovalue-OO base class modelling a console as an in/out pair with settled capability facts. Objects are plain Tcl list values;-virtualmethods dispatch on a class tag so subclasses can override everything channel-related. Cooperative seams are pluggable: the probe-byte store (waiting_chunks_arrayvar), the size-query provider (size_query_provider), and an anchor lifecycle callback. Source:src/modules/opunk/AGENTS.md.punk::console(src/modules/punk/console-999999.0a1.0.tm) - the integrating layer: console-spec resolution, per-console terminal-property facts, raw mode, ANSI emit/query helpers, and registration of the providers the base class consumes.- Spec forms and selection. A console spec is an
{in out}channel pair, an anchored instance name, or an::opunk::Consoleobject value, normalised bypunk::console::console_spec_resolve.repl::init -console <spec>selects the console per repl (G-001, achieved); the default is the process console{stdin stdout}. Source:src/modules/punk/AGENTS.md. - Backends (G-001, achieved). Reference subclasses
opunk::console::test,::ssh,::tk(deterministic test double, socket-carried terminal, Tk text widget) prove non-detectable terminal-like devices can host a repl with no base-class orpunk::consolechanges. Detail:goals/archive/G-001-pluggable-console-backends.md. - Location transparency (G-007, achieved). Facts are tsv-backed and terminal queries are owner-routed: a query from any thread forwards to the console-owning thread through a
thread::sendchoke point, so code-interp code sees the same console the repl sees withoutrepl evalround-trips. Ownership lives in a tsv registry keyed by canonical channel pair. - Raw mode. twapi on Windows when available; otherwise a persistent powershell console-mode named-pipe server serves raw transitions on twapi-less runtimes (G-106, achieved) - quiet, lazy-started, exits with the owning process.
- Dead-console handling. The Tcl 9 Windows console driver never reported a killed host terminal to the script, leaving orphaned shells spinning a core (G-039, achieved; upstream ticket filed and fixed). The repl arms a close-on-probe-failure watchdog only on runtimes a
punk::lib::checkclassifier reports as affected (G-076, achieved); runtimes at or past 9.0.5 deliver the dead console as a readable event and use the normal eof teardown.
Module ecosystem and package loading
- Editable modules live in
src/modules/(+src/modules_tcl8/,src/modules_tcl9/siblings); filenames carry the magic dev version999999.0a1.0with the real semver in a<name>-buildversion.txtsidecar, and namespaces mirror directory layout (punk/ansi/=punk::ansi).punk::libunknownis the manually-versioned exception. Source:src/modules/AGENTS.md. punk::argsis both parser and documentation system.PUNKARGS/argdocblocks travel with each proc and power inline usage tables (i <command>at the repl); even manually-parsing procs carry documentation-only definitions. Much of the goals-era work has been hardening this system - see thepunkargsentries acrossGOALS.mdandGOALS-archive.md.- Discovery and modpods.
punk::libunknownprovides module discovery/registration beyondtcl::tmdefaults;#modpod-*source directories pack into zip-based multi-file.tmmodules at build time (src mode registers them via an inline boot scanner written in builtins). - Bootsupport.
src/bootsupport/holds snapshot copies of build-time-critical modules (manifest:include_modules.config), analogous to devDependencies.make.tclclassifies snapshot staleness for five runtime-critical packages (punkcheck,punk::repo,punk::mix,punk::tdl,punk::args) as abort/prompt/warn from the version delta. Source:src/bootsupport/AGENTS.md. - opunk layer.
src/modules/opunk/exploresvoo-based value-OO reimplementations (objects as plain Tcl values); thepunk::*modules remain the production implementations. - Vendored code. Third-party packages live in
src/vendormodules/andsrc/vendorlib/(+_tcl8/_tcl9siblings), refreshed bymake.tcl vendorupdatefrom config; never hand-edited (enforcement policy is proposed as G-026).
Build, provenance and kits
src/make.tclis the single build entry. Key subcommands:modules/libs/packages(repo-root build outputs),vfscommonupdate(regenerate_vfscommon.vfs),bake(kit assembly intobin/; optional kit names confine the run to those kits - G-121),bakelist(report the configured kit matrix with runtime/vfs presence and deployed state),bin,bakehouse(packages + bake for a clean checkout;project/vfsremain as deprecated aliases - G-112 rename, achieved),bootsupport(snapshot refresh),vendorupdate,check,workflow,projectversion. Dispatch and help dogfoodpunk::args(G-030, achieved) and degrade to plain scan/help when bootsupportpunk::argsis stale (PUNKBOOT_PLAIN=1forces the degraded mode). Source:src/AGENTS.md.- Host vs target platform (G-122). The kit surfaces separate what the driving tclsh IS from what a bake is FOR. Target-keyed: the
bin/runtime/<tier>store a runtime is read from,.exesuffixing of runtimes and kit outputs, presence checks, and the pre-deploy process sweep's tooling (tasklist/taskkillvsps/kill, skipped when the target's processes cannot exist on this host). Host-keyed: copy commands, path handling, filesystem case rules, prompts. The default target is the host canon except for cygwin-family hosts (msys2/cygwin-runtime tclsh -tcl_platform(platform)unixon windows), which targetwin32-x86_64; amapvfs.configentry may declare its own target and is then addressed in that platform's tier.make.tcl checkprints the derivation. Zip-type kits assemble without zipfs in the driving tcl (raw-runtime split +punk::zip::mkzip+ concatenation), and since G-124 they EXTRACT without it too:punk::zipreads a zip - plain, or attached to an executable under either offset convention - with stock Tcl only, so a bake needs neither zipfs nor tcllib to carry a runtime'stcl_libraryinto the kit. Sources:src/AGENTS.md,src/runtime/AGENTS.md,src/modules/punk/platform-999999.0a1.0.tm,src/modules/punk/zip-999999.0a1.0.tm. - punkcheck records every install/delete as events in per-folder
.punkcheckdirectories - the basis for skip/copy change detection, superseded-module pruning and provenance. Single OO record lifecycle (G-094, achieved) with atomic saves and advisory event-scoped locking for concurrent writers (G-095, achieved). - Provenance gates. Build/promotion commands warn on uncommitted
src/changes (column-0PROVENANCE-WARNING:token,-dirty-abortfor strict mode);vendorupdatewarns for dirty source-project checkouts. - Boot-precondition gate (G-125). A bake refuses a kit whose merged
.vfssupplies no tcl library rather than deploying an artifact that cannot initialise: the kit lands inFAILED KITSand nothing is written, so the previously deployedbin/<kit>survives. Structural and non-executing (so cross-target kits are covered), reading the merged tree rather than the extraction outcome. The predicate ispunkboot::utils::vfs_boot_library_report, called through the same guarded require as the provenance check so a stale bootsupport snapshot degrades it to a notice;make.tcl checkreports ACTIVE/UNAVAILABLE. Sources:src/AGENTS.md,src/modules/punkboot/utils-999999.0a1.0.tm. - Payload/target consistency checks (G-133 + G-134), all advisory. At the same post-merge seam, a bake classifies each binary library in the merged tree by header (
punkboot::utils::binary_arch_classify- PE/ELF/Mach-O, honest unknowns) against the kit's target platform; wrong-arch libraries outside platform-discriminated subdirs (canonical<os>-<cpu>names, vendor spellings likewin-x64) earn recappedBUILD-WARNINGs. After assembly, a kit declaring smoke-require packages (mapvfs.config5th entry element) has each one plain-package required inside the freshly built artifact via its tclsh subcommand - the only check that sees resolution-order defects (wrong-arch version shadowing); cross-target kits skip with a stated reason. The assembled image is also probed for its zip offset convention (G-134,punkboot::utils::kit_offsetstyle_reportoverpunk::zip::archive_info): a FILE-relative attached payload warns (the pipeline emits archive-relative; the G-128 stamper refuses file-relative by default), no-zip/plain results stay silent. Same guarded-require degradation;make.tcl checkreports all three. None guarantees statics, pure-tcl packages with binary deps, or version preference beyond the declared smoke set. Sources:src/AGENTS.md,src/runtime/AGENTS.md,src/modules/punkboot/utils-999999.0a1.0.tm. - Kit icon step (G-057 + G-128). Every kit a bake builds gets a
<kitname>.resources.tomlsidecar (deployed besidebin/<kit>) recording the build-time icon choice - defaultsrc/runtime/punkshell.ico, overridable by a rootpunkshell.icoin the kit's own custom.vfsfolder (pre-merge) - with sha256 identity and provenance lifted from the icon's G-135 assetorigin record. win32-target kits get the icon embedded as PERT_ICON/RT_GROUP_ICONbehind a single seam (::punkboot::kit_icon_process): the vendored punkres portable stamper (src/tools/punkres, built tobin/punkres(.exe)by the make.tcl tool step) is selected when present and works from any build host; otherwise the twapi arm (tcl-sfe mechanism) serves windows hosts with nothing built. Both arms share one semantic (delete all icon/group entries, write ids 1..N; the group is NAMED from the icon file's uppercased rootname -PUNKSHELL- with language adopted from the replaced group) and stamp a per-kit copy of the payload-free raw runtime prefix BEFORE payload attach; punkres can also stamp a FINISHED kit post-hoc, preserving an appended zip payload (archive-relative moved verbatim; file-relative refused by default, shifted with a consent flag). Non-PE targets skip as not applicable; a host where neither mechanism serves skips with a combined notice naming the punkres build remedy; the sidecar is written in every case. Sources:src/AGENTS.md,bin/AGENTS.md,src/vfs/AGENTS.md,src/tools/AGENTS.md. - Buildsuites and the kit family.
src/buildsuites/suite_tcl90/builds Tcl/Tk/tcllib from source with a pinned zig toolchain and produces the runtime kit family (plain / punk / bi) plus artifact metadata (G-096-G-117 era: see archived goals G-096, G-098, G-102, G-103, G-107); artifacts publish to the punkbin repo thatbin/punk-runtime.cmdfetches from.src/buildsuites/suite_tcl86/is the 8.6 sibling (G-099 + G-100, both achieved): a Tcl 8.6 windows runtime (static + dynamic shells,tcl86t.dll, on-disk lib tree - no zipfs, so no self-contained kit) with thread 2.8 + tclvfs + Tk 8.6 + tklib + tcllib(+tcllibc critcl accelerators) companions, gated against the 8.6 core/thread/tclvfs testsuites with tcllib/tklib on a record tier and an opt-intest-tk; punkshell's own runtests is censused on that runtime at parity with a same-day native-8.6 baseline. Suite child shells scrubTCL<major>_<minor>_TM_PATHas well as TCLLIBPATH/TCL_LIBRARY/TK_LIBRARY - without it a census measures the machine's module trees. The 8.6 kit container strategy remains in-flux - see "In-flux areas". - Project generation.
dev project.newcomposes thin layouts fromsrc/project_layouts/(overlay chain with.antideletion markers andname@basederivation - G-087, achieved) and injects bootsupport modules from the generating shell at generation time. - Workflow overview.
tclsh src/make.tcl workflowprints the embedded ASCII data-flow overview of the build/release pipeline, with its own update contract insrc/AGENTS.md. This section deliberately summarises rather than copies it.
Test harness
src/tests/runtests.tclis the entry point. Path-glob discovery overtestsuites/with theX/***-style lattice (-include-paths/-exclude-paths, G-093), single-process or multi-process child modes, a-jobs Nworker pool (G-091), report modes (compact/markdown/json) and an optional UDP watch stream. Source:src/tests/AGENTS.md.- Characterization style. Suites pin observed current behaviour, including
_GAP_-named tests that pin known bugs and flip when a goal fixes them - goal acceptance criteria routinely name the pins they flip. - Child-process suites by design. A repl cannot run inside the shared testinterp (codethread exit callbacks target the thread's main interp), so console/repl verification drives child processes (
src/tests/testsupport/repl_console_driver.tcl); kit and launcher behaviour is likewise exec-driven undersrc/tests/shell/. - Mode parity.
scriptlib/developer/runtests_parity.tclcompares-report jsonoutputs across runner modes.
In-flux areas
Mechanics here are documented as pointers only - detailed prose would be obsolete on arrival. Read the goal detail files for intended end states and the code for current state.
- Subshell roadmap: G-002, G-003, G-008, G-009, G-010 (non-nested subshells, resource limits, scoped console state, themed profiles, tree navigation).
- 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 (
-eone-liners), 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
- How it works now (durable mechanics): this file.
- What you must do when working here: root
AGENTS.md+ the child AGENTS.md chain (binding). - What changed and why:
GOALS.md,GOALS-archive.md,goals/detail files;CHANGELOG.mdfor release-level history. - Per-command and per-proc usage: PUNKARGS blocks in the modules (
i <command>at a repl); generated docs undersrc/doc/. - User-facing features:
README.md.