Browse Source

punk::args 0.12.6 + punk::ns 0.9.0: LLM-oriented usage output; tcl-whatis agent skill

punk::ns 0.9.0: cmdhelp (i) gains -return text - plain-text argument
documentation for machine/LLM consumption: no ANSI, no table layout, the
argument section of every command form rendered in one output under a common
header ('i -return text after' shows all six forms), G-041 form selection
still honoured when argument words are supplied, no advisory-parse error
preamble (-return dict remains the parse-status surface). Implementation
delegates resolution to the -return dict recursion and renders per-form via
the string renderer.

punk::args 0.12.6 (user direction; string-form layout explicitly not frozen -
no external consumers yet):
- arg_error renders the error message + per-form match detail exactly once,
  BELOW the usage display (was above and below); bottom append suppressed for
  empty messages.
- string renderer argument rows are single-line
  'name  TYPE:type(constraints) ?DEFAULT:'val'? ?MULTI:yes?' - type
  constraints parenthesize onto the base type instead of line-breaking
  mid-row; empty DEFAULT/MULTI labels and whitespace-only help blocks omitted.
- choices render as markdown-subset bullets '- `choice` - label' with label
  continuations at a fixed deeper indent - choice names are now
  machine-distinguishable from label text without ANSI. Structural contract:
  column-0 lines bearing TYPE: are argument rows, indented lines are
  documentation, backticked bullets are choices. Table cells unchanged.

New scriptlib/developer/whatis.tcl (runtime command introspection for
agents): resolved name, subcommand-chain resolution via punk::ns::cmdinfo
('whatis string is'), kind, providing package + loaded version, origin file,
proc definition file:line, punk::args synopsis; -body appends the runtime
body with file-relative line numbers, -doc the full plain-text argdoc via
cmdhelp -return text (with fallback for older punk::ns). Backing the
cross-framework tcl-whatis agent skill: canonical .agents/skills/tcl-whatis/
(Agent Skills standard - pi/OpenCode/Codex) with byte-identical copy in
.claude/skills/ for Claude Code; .gitignore gains !.claude/skills/ with the
fossil ignore-glob comment + exception-set docs updated per the dual-VCS
contract (fossil adds staged). Root AGENTS.md: .agents/skills index entry,
whatis pointer in Repo-wide Notes, scriptlib/developer example.

Tests: new pins cmdhelp_return_text_all_forms/_argword_form_selection/
_single_form_plain_marker (cmdhelp.test), rendering_stringmode_singleline_argrows
+ rendering_stringmode_choice_bullets (rendering.test); deliberate-change pin
updates: errorselection.test message matches gained leading * and cmdhelp.test
message pins sample the last line (message placement). Full source-tree suite
963/979 pass, sole failure the pre-existing exec-14.3 baseline; make.tcl
modules clean.

Project version 0.12.42 -> 0.13.0 with CHANGELOG.md entry.

Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
master
Julian Noble 1 week ago
parent
commit
75cffe21f3
  1. 78
      .agents/skills/tcl-whatis/SKILL.md
  2. 78
      .claude/skills/tcl-whatis/SKILL.md
  3. 2
      .fossil-settings/AGENTS.md
  4. 2
      .fossil-settings/ignore-glob
  5. 4
      .gitignore
  6. 4
      AGENTS.md
  7. 6
      CHANGELOG.md
  8. 2
      punkproject.toml
  9. 310
      scriptlib/developer/whatis.tcl
  10. 98
      src/modules/punk/args-999999.0a1.0.tm
  11. 3
      src/modules/punk/args-buildversion.txt
  12. 73
      src/modules/punk/ns-999999.0a1.0.tm
  13. 3
      src/modules/punk/ns-buildversion.txt
  14. 13
      src/tests/modules/punk/args/testsuites/args/errorselection.test
  15. 73
      src/tests/modules/punk/args/testsuites/args/rendering.test
  16. 59
      src/tests/modules/punk/ns/testsuites/ns/cmdhelp.test

78
.agents/skills/tcl-whatis/SKILL.md

@ -0,0 +1,78 @@
---
name: tcl-whatis
description: Use when you need to know how to call a Tcl command in this repo (including ensemble subcommands like 'string is'), where a proc is defined (file and line number), which module version is actually loaded, or what a proc's runtime body is. Runtime introspection via punkshell - reports resolved name, kind (proc/alias/ensemble), providing package + version, origin file, def file:line, and punk::args synopsis. Cheaper and more accurate than grep+read for interface questions, and the only option for runtime-generated procs.
---
# tcl-whatis
Runtime introspection for Tcl commands in the punkshell repo. Asks the live
interpreter instead of grepping source, so it resolves aliases, ensembles and
subcommand chains, autoloads the providing module, and reports which of the
repo's several module copies (src/modules vs bootsupport vs kit-internal
zipfs) is actually loaded.
## Command
Run from the repo root (`punk91` lives in `bin/`; any punkshell kit works):
punk91 src script scriptlib/developer/whatis.tcl ?-body? ?-doc? <command> ?subcommand?...
- `<command>` - Tcl command name, qualified or not (e.g. `punk::args::parse`,
`corp`). If not already loaded, the longest namespace-prefix package match
is `package require`d automatically.
- `?subcommand?...` - ensemble/subcommand words: `whatis string is` resolves
the chain to `::tcl::string::is` (via punk::ns::cmdinfo) and reports that
origin's synopsis - no prior knowledge of the implementing namespace needed.
Unresolvable trailing words are reported in a `warning:` line.
- `-body` - append the runtime proc body. Line numbers are file-relative when
the `def:` line was located, so they match the origin file exactly.
- `-doc` - append the full argument documentation as plain ANSI-free text
with the argument section of EVERY command form (punk::ns::cmdhelp
-return text). Large for big commands; prefer the default synopsis unless
you need per-argument help text, defaults and choices.
Exit codes: 0 ok, 1 command not found, 2 usage error.
## Output fields
command: string is the queried words
alias: -> punk::ns::cmdhelp alias chain, if any
resolves: ::tcl::string::is chain/alias resolution target
kind: proc | ensemble | native command
package: punk::args 999999.0a1.0 providing package + loaded version
origin: C:/repo/.../src/modules/punk/args-999999.0a1.0.tm
def: <origin-file>:6528 proc definition line in origin file
synopsis: one usage line per command form (from the punk::args definition)
Ensembles report their namespace and subcommand map instead of `def:`.
## -doc output structure (parsing contract)
- Column-0 lines containing `TYPE:` are argument rows, one line each:
`name TYPE:type(constraints) ?DEFAULT:'val'? ?MULTI:yes?`. Optional
arguments show as `?name?`, repeatable ones as `name...`.
- Indented lines under a row are that argument's help text (never flags,
even when a line starts with `-word`).
- Choices are markdown-style bullets: `` - `choice` - label text``, with
label continuation lines indented past the bullet dash. The backticked
token is the choice (it may carry a trailing marker glyph); everything
after the closing backtick is label/documentation.
- Multiform commands render one ` --ARGUMENTS (form 'name')-- ` section per
form under a single common header (description + all-forms synopsis).
## Caveats
- Keep the `src` launch argument: it loads the working-tree modules under
`src/modules` (versions like 999999.0a1.0) so `origin`/`def` point at
editable files. Without it the kit-stamped copies inside the executable are
introspected (`//zipfs:/...` paths, older versions) and the report says so
in a `warning:` line.
- Load noise ("src mode: registered ...") goes to stderr; read stdout only
(append `2>/dev/null` in bash, `2>$null` in PowerShell).
- To edit a proc, go to the `def:` file:line and Read the file there; do not
build edits from `-body` output text.
- `def: (no proc definition found ...)` means the proc was generated at
runtime - `-body` output is then the only source.
- Trust `origin:` over assumptions about which module copy is loaded; this
repo has several coexisting copies of many modules and ties do not always
resolve the way you expect.

78
.claude/skills/tcl-whatis/SKILL.md

@ -0,0 +1,78 @@
---
name: tcl-whatis
description: Use when you need to know how to call a Tcl command in this repo (including ensemble subcommands like 'string is'), where a proc is defined (file and line number), which module version is actually loaded, or what a proc's runtime body is. Runtime introspection via punkshell - reports resolved name, kind (proc/alias/ensemble), providing package + version, origin file, def file:line, and punk::args synopsis. Cheaper and more accurate than grep+read for interface questions, and the only option for runtime-generated procs.
---
# tcl-whatis
Runtime introspection for Tcl commands in the punkshell repo. Asks the live
interpreter instead of grepping source, so it resolves aliases, ensembles and
subcommand chains, autoloads the providing module, and reports which of the
repo's several module copies (src/modules vs bootsupport vs kit-internal
zipfs) is actually loaded.
## Command
Run from the repo root (`punk91` lives in `bin/`; any punkshell kit works):
punk91 src script scriptlib/developer/whatis.tcl ?-body? ?-doc? <command> ?subcommand?...
- `<command>` - Tcl command name, qualified or not (e.g. `punk::args::parse`,
`corp`). If not already loaded, the longest namespace-prefix package match
is `package require`d automatically.
- `?subcommand?...` - ensemble/subcommand words: `whatis string is` resolves
the chain to `::tcl::string::is` (via punk::ns::cmdinfo) and reports that
origin's synopsis - no prior knowledge of the implementing namespace needed.
Unresolvable trailing words are reported in a `warning:` line.
- `-body` - append the runtime proc body. Line numbers are file-relative when
the `def:` line was located, so they match the origin file exactly.
- `-doc` - append the full argument documentation as plain ANSI-free text
with the argument section of EVERY command form (punk::ns::cmdhelp
-return text). Large for big commands; prefer the default synopsis unless
you need per-argument help text, defaults and choices.
Exit codes: 0 ok, 1 command not found, 2 usage error.
## Output fields
command: string is the queried words
alias: -> punk::ns::cmdhelp alias chain, if any
resolves: ::tcl::string::is chain/alias resolution target
kind: proc | ensemble | native command
package: punk::args 999999.0a1.0 providing package + loaded version
origin: C:/repo/.../src/modules/punk/args-999999.0a1.0.tm
def: <origin-file>:6528 proc definition line in origin file
synopsis: one usage line per command form (from the punk::args definition)
Ensembles report their namespace and subcommand map instead of `def:`.
## -doc output structure (parsing contract)
- Column-0 lines containing `TYPE:` are argument rows, one line each:
`name TYPE:type(constraints) ?DEFAULT:'val'? ?MULTI:yes?`. Optional
arguments show as `?name?`, repeatable ones as `name...`.
- Indented lines under a row are that argument's help text (never flags,
even when a line starts with `-word`).
- Choices are markdown-style bullets: `` - `choice` - label text``, with
label continuation lines indented past the bullet dash. The backticked
token is the choice (it may carry a trailing marker glyph); everything
after the closing backtick is label/documentation.
- Multiform commands render one ` --ARGUMENTS (form 'name')-- ` section per
form under a single common header (description + all-forms synopsis).
## Caveats
- Keep the `src` launch argument: it loads the working-tree modules under
`src/modules` (versions like 999999.0a1.0) so `origin`/`def` point at
editable files. Without it the kit-stamped copies inside the executable are
introspected (`//zipfs:/...` paths, older versions) and the report says so
in a `warning:` line.
- Load noise ("src mode: registered ...") goes to stderr; read stdout only
(append `2>/dev/null` in bash, `2>$null` in PowerShell).
- To edit a proc, go to the `def:` file:line and Read the file there; do not
build edits from `-body` output text.
- `def: (no proc definition found ...)` means the proc was generated at
runtime - `-body` output is then the only source.
- Trust `origin:` over assumptions about which module copy is loaded; this
repo has several coexisting copies of many modules and ties do not always
resolve the way you expect.

2
.fossil-settings/AGENTS.md

@ -25,7 +25,7 @@ Versioned fossil settings (one setting value or glob list per file) for this dua
### ignore-glob derivation rules (verified against fossil 2.28)
- **No negation exists in fossil globs.** Git's `/bin/*` + `!exception` pattern becomes: ignore `bin/*` wholesale and explicitly `fossil add` the tracked exceptions (ignore-glob never affects already-managed files, so they stay tracked). The exception set is whatever git tracks under an ignored tree - at the time of writing: `bin/AGENTS.md`, `bin/*.cmd`, `bin/*.kit`, `bin/*.tcl`, `bin/*.sh`, `bin/*.bash`, `.claude/settings.json` (shared claude harness hooks/permissions; session-local `.claude` files stay ignored), plus force-tracked one-offs (`bin/libssp-0.dll`, `src/vfs/punk9magicsplat.vfs/lib/nmake/x86_64-w64-mingw32-nmakehlp.exe`). Derive the current set with the verification comparison below rather than trusting this list.
- **No negation exists in fossil globs.** Git's `/bin/*` + `!exception` pattern becomes: ignore `bin/*` wholesale and explicitly `fossil add` the tracked exceptions (ignore-glob never affects already-managed files, so they stay tracked). The exception set is whatever git tracks under an ignored tree - at the time of writing: `bin/AGENTS.md`, `bin/*.cmd`, `bin/*.kit`, `bin/*.tcl`, `bin/*.sh`, `bin/*.bash`, `.claude/settings.json` (shared claude harness hooks/permissions; session-local `.claude` files stay ignored), `.claude/skills/**` (project agent skills - synced copies of `.agents/skills/**`, which is not git-ignored and needs no exception), plus force-tracked one-offs (`bin/libssp-0.dll`, `src/vfs/punk9magicsplat.vfs/lib/nmake/x86_64-w64-mingw32-nmakehlp.exe`). Derive the current set with the verification comparison below rather than trusting this list.
- A bare directory name prunes that whole tree; `*` crosses `/`; git patterns intended to match at any depth need an additional `*/` variant; `#` comment lines are honoured.
- Do not add the fossil-generated names (`manifest`, `manifest.uuid`, `manifest.tags`) to `ignore-glob` - fossil handles them itself.
- **Nested `.gitignore` files are git-only**: ignore files inside subtrees (e.g. the project-layout templates under `src/project_layouts/`) are honoured by git as nested ignores of the outer repo but invisible to fossil - template content they match is untracked in git yet managed by fossil. Treat such divergence as a signal to review the git side (usually `git add -f` of the affected template files).

2
.fossil-settings/ignore-glob

@ -22,7 +22,7 @@ tmp
.vscode
*/.vscode
#claude harness config: shared .claude/settings.json is a tracked exception by explicit add (see header)
#claude harness config: shared .claude/settings.json and .claude/skills/ files are tracked exceptions by explicit add (see header)
.claude/*
*/.claude
.omo

4
.gitignore vendored

@ -23,9 +23,11 @@
/tmp/
.vscode
#claude harness config: track the shared project settings (hooks/permissions), ignore everything else (session-local settings etc)
#claude harness config: track the shared project settings (hooks/permissions) and project skills,
#ignore everything else (session-local settings etc)
.claude/*
!.claude/settings.json
!.claude/skills/
.omo
/logs/

4
AGENTS.md

@ -190,13 +190,14 @@ The project version is fully independent of module versions. A module bump (even
- `src/testansi/` — Sample ANSI art files (do not modify)
- `bin/` — Built punk shell executables, launch package modes (incl. `src` mode for working-tree verification), plain runtime kits (see bin/AGENTS.md)
- `.fossil-settings/` — Versioned fossil settings and the git+fossil dual-VCS coexistence contract: ignore-sync rules, files neither system may track (see .fossil-settings/AGENTS.md)
- `.agents/skills/` — Project agent skills in the cross-framework Agent Skills SKILL.md format (read natively by pi, OpenCode, Codex and others). Canonical home; `.claude/skills/` holds byte-identical copies for Claude Code discovery - edit under `.agents/skills/` and re-copy, verify sync with a plain diff (no child AGENTS.md)
- `GOALS.md` — Top-level technical goal index, summary-only (ID, status, title, scope, detail pointer per goal); required read for non-trivial work (no child AGENTS.md; the file documents its own format, read workflow, and the agent goal-authoring workflow)
- `GOALS-archive.md` — Summary records of achieved goals moved out of the active index (historical context only)
- `goals/` — Per-goal detail files carrying the canonical Goal/Acceptance contract prose plus supporting detail (see goals/AGENTS.md)
- `goals/archive/` — Detail files for achieved/archived goals
- Directories agents should not directly modify (no child DOX needed):
- `callbacks/` — Experimental shellspy features, user-only
- `scriptlib/` — Shared utilities + manual tests, user-only. EXCEPTIONS: `scriptlib/_punktest/` is test-owned (fixtures for `src/tests/shell/testsuites/punkexe/scriptexec.test`, resolved via `lib:_punktest/<name>`); agents may manage that subfolder as part of test work. `scriptlib/developer/` holds agent-authored developer showcase/demo apps and developer utility scripts (created at user request 2026-07-11, e.g `tkconsole_demo.tcl` for the G-001 tk console backend, `goals_lint.tcl` for the goals doc contracts per goals/AGENTS.md Verification); agents may add or update entries there when the user asks for one. The rest of `scriptlib/` stays user-only.
- `scriptlib/` — Shared utilities + manual tests, user-only. EXCEPTIONS: `scriptlib/_punktest/` is test-owned (fixtures for `src/tests/shell/testsuites/punkexe/scriptexec.test`, resolved via `lib:_punktest/<name>`); agents may manage that subfolder as part of test work. `scriptlib/developer/` holds agent-authored developer showcase/demo apps and developer utility scripts (created at user request 2026-07-11, e.g `tkconsole_demo.tcl` for the G-001 tk console backend, `goals_lint.tcl` for the goals doc contracts per goals/AGENTS.md Verification, `whatis.tcl` runtime command introspection backing the `tcl-whatis` agent skill); agents may add or update entries there when the user asks for one. The rest of `scriptlib/` stays user-only.
- `bin/` — Built binaries and helpers, build output target. This includes the polyglot `.cmd` launcher/utility scripts (e.g `bin/runtime.cmd`): they are GENERATED by the punk::mix scriptwrap machinery from sources under `src/scriptapps/` — a request to "fix bin/<name>.cmd" means editing `src/scriptapps/<name>.*` + `<name>_wrap.toml` and re-wrapping (see bin/AGENTS.md), never editing the output
- `modules/` (root) — Build output target for `tclsh src/make.tcl modules`
- `lib/` (root) — Build output target for `tclsh src/make.tcl libs`
@ -210,6 +211,7 @@ The project version is fully independent of module versions. A module bump (even
- The project extends Tcl with ANSI terminal capabilities, functional and pattern-matching language features, and a comprehensive module ecosystem.
- This root file is intentionally limited to DOX governance, global ownership boundaries, and the top-level Child DOX Index.
- Source-tree build, testing, linting, and file-resolution workflow lives in `src/AGENTS.md`.
- Runtime Tcl command introspection for agents (resolved name/kind, subcommand-chain resolution e.g `string is`, providing package + loaded version, origin file, def file:line, punk::args synopsis; `-doc` = full plain-text argdoc of all forms): `punk91 src script scriptlib/developer/whatis.tcl ?-body? ?-doc? <command> ?subcommand?...` - full usage and caveats in `.agents/skills/tcl-whatis/SKILL.md`.
- Tcl module authoring conventions live in `src/modules/AGENTS.md` and closer module child docs.
- If AGENTS.md conflicts with CLAUDE.md, AGENTS.md wins.
- Technical project goals live in root `GOALS.md` (summary index: ID, status, title, scope, detail pointer per goal) with the canonical Goal/Acceptance contract prose in `goals/G-<id>-<slug>.md` detail files - read the detail file of every goal whose scope intersects paths being edited. Goals are user-owned: agents add or edit goal contract elements (in either tier) only at the user's request, proposal-first (show the proposed wording, get explicit approval before writing - see the `GOALS.md` maintenance rules); never on their own initiative. Suggesting candidate goals when work surfaces something goal-worthy is welcome and encouraged - as a flagged proposal, not a file edit. Non-contract detail-file updates from user-directed work need only be reported in the completion summary. Agents auto-flip a goal to `achieved <date>` when its detail-file acceptance criterion is met as part of the DOX closeout pass - the flip includes archiving the entry to `GOALS-archive.md` and the detail file to `goals/archive/` - and must flag `proposed`→`active` transitions in their completion report for user confirmation. See `GOALS.md` for the full workflow, including how to author a new goal when asked.

6
CHANGELOG.md

@ -5,6 +5,12 @@ 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.13.0] - 2026-07-18
- punk::ns 0.9.0: cmdhelp (`i`) gains `-return text` - plain-text argument documentation for machine/LLM consumption: no ANSI codes, no table layout, and the argument section of every command form rendered in a single output under a common header (`i -return text after` shows all six forms), with G-041 form selection still honoured when argument words are supplied. No advisory-parse error preamble (use `-return dict` for parse status).
- punk::args 0.12.6: usage/error display refinements (user direction; the string-form layout is explicitly not frozen - no external consumers yet). The error message and its per-form match detail now render exactly once, BELOW the usage display (previously above and below). The string renderer's argument rows are single-line `name TYPE:type(constraints) ?DEFAULT:'val'? ?MULTI:yes?` - type constraints parenthesize onto the base type instead of line-breaking mid-row, and empty DEFAULT/MULTI labels and whitespace-only help blocks are omitted. Choices render as markdown-subset bullets (`` - `choice` - label``) with label continuations at a fixed deeper indent, so choice names are machine-distinguishable from label text without ANSI (structural contract: column-0 lines bearing TYPE: are argument rows, indented lines are documentation, backticked bullets are choices).
- New developer/agent introspection script `scriptlib/developer/whatis.tcl` (backing the checked-in cross-framework `tcl-whatis` agent skill in `.agents/skills/` with a synced copy in `.claude/skills/`): reports a command's resolved name, subcommand-chain resolution (e.g `whatis string is`), kind, providing package + loaded version, origin file, proc definition file:line, punk::args synopsis, and optionally the runtime body with file-relative line numbers (`-body`) or the full plain-text argdoc (`-doc`).
## [0.12.42] - 2026-07-17
- make.tcl: new informational subcommand `workflow` - prints an embedded plain-ASCII overview of the build/release data flow (release-ready command sequence, source->built-packages->bootsupport/kit-vfs/kit folder diagrams, keyed notes on version stamping, punkcheck provenance, the staleness gate, -confirm and locked-exe deploys). punk::args-documented like the other subcommands (tabled help + PUNKBOOT_PLAIN fallback covered); exempt from the bootsupport staleness gate alongside check/projectversion. The text lives in ::punkboot::workflow_text so it travels with make.tcl into generated-project layouts; update contract for keeping it current documented in src/AGENTS.md Work Guidance and in the output's MAINTENANCE key.

2
punkproject.toml

@ -1,4 +1,4 @@
[project]
name = "punkshell"
version = "0.12.42"
version = "0.13.0"
license = "BSD-2-Clause"

310
scriptlib/developer/whatis.tcl

@ -0,0 +1,310 @@
#!/usr/bin/env tclsh
# whatis.tcl - runtime introspection report for a command, aimed at agent/LLM use
#
# Reports for a command name (possibly unqualified, possibly followed by
# subcommand words - e.g 'string is', 'dict getdef'):
# - resolved fully-qualified name and kind (proc / alias / ensemble / native)
# - subcommand-chain resolution via punk::ns::cmdinfo (ensembles, aliases,
# nested chains) with any unresolvable trailing words reported
# - providing package + version and the origin file the loaded copy came from
# - file:line of the proc definition in that origin file (when locatable)
# - punk::args synopsis (compact usage line per form)
# Options:
# -body append the runtime proc body, numbered with file-relative line numbers
# when the definition line was located (numbers then match the origin file)
# -doc append the full argument documentation for all forms as plain text
# (punk::ns::cmdhelp -return text)
#
# Must run inside a punkshell interpreter (needs punk::args, punk::ansi, punk::ns):
# punk91 src script scriptlib/developer/whatis.tcl ?-body? ?-doc? <command> ?subcommand?...
# Use the 'src' launch argument so the working-tree modules under src/modules are
# loaded; without it the kit-stamped copies inside the executable (//zipfs:/...)
# are introspected and file:line will not refer to editable files.
# Exit 0 on success; 1 command not found; 2 usage error.
proc strip {text} {
if {[info commands ::ansistrip] ne ""} {
return [ansistrip $text]
} elseif {![catch {package require punk::ansi}]} {
return [punk::ansi::ansistrip $text]
}
return $text
}
proc out {key val} {
puts [format "%-9s %s" $key: [strip $val]]
}
# longest namespace-prefix of a fully-qualified command that names a package
proc pkg_for_command {fqcmd} {
set parts [lrange [split [string trimleft $fqcmd :] ::] 0 end]
set parts [lsearch -all -inline -not $parts {}] ;# split on :: leaves empties
for {set n [expr {[llength $parts]-1}]} {$n >= 1} {incr n -1} {
set prefix [join [lrange $parts 0 $n-1] ::]
if {![catch {package present $prefix} ver]} {
return [list $prefix $ver]
}
}
return {}
}
# attempt to load a package matching the namespace prefixes of $name
proc try_autoload {name} {
set parts [lsearch -all -inline -not [split [string trimleft $name :] ::] {}]
for {set n [expr {[llength $parts]-1}]} {$n >= 1} {incr n -1} {
set prefix [join [lrange $parts 0 $n-1] ::]
if {![catch {package require $prefix}]} {
return $prefix
}
}
return {}
}
# origin file of a loaded package, parsed from its ifneeded script
proc pkg_origin {pkg ver} {
set script [package ifneeded $pkg $ver]
if {[regexp {[;\n]?\s*source\s+(?:-encoding\s+\S+\s+)?(.+?)\s*$} $script -> path]} {
return [string trim $path {"{}}]
}
if {[regexp {load\s+(\S+)} $script -> path]} {
return $path
}
return ""
}
# locate the proc definition line in a source file; returns list of line numbers
proc find_def_lines {path fqcmd} {
if {$path eq "" || [catch {open $path r} fd]} {
return {}
}
set data [read $fd]
close $fd
set tail [namespace tail $fqcmd]
set noco [string trimleft $fqcmd :]
set exact {}
set bytail {}
set ln 0
foreach line [split $data \n] {
incr ln
if {[regexp {^\s*proc\s+(:{0,2}[\w:]+)\s} $line -> pname]} {
set pclean [string trimleft $pname :]
if {$pclean eq $noco} {
lappend exact $ln
} elseif {[namespace tail $pclean] eq $tail} {
lappend bytail $ln
}
}
}
if {[llength $exact]} {
return $exact
}
return $bytail
}
# ---------------------------------------------------------------- arg handling
set opt_body 0
set opt_doc 0
set names {}
foreach a $argv {
switch -- $a {
-body {set opt_body 1}
-doc {set opt_doc 1}
default {lappend names $a}
}
}
if {![llength $names]} {
puts stderr "usage: punk91 src script scriptlib/developer/whatis.tcl ?-body? ?-doc? <command> ?subcommand?..."
exit 2
}
set name [lindex $names 0]
set subwords [lrange $names 1 end]
# ---------------------------------------------------------------- resolution
set resolved [uplevel #0 [list namespace which -command $name]]
if {$resolved eq ""} {
set loaded [try_autoload $name]
set resolved [uplevel #0 [list namespace which -command $name]]
if {$resolved eq "" && $loaded ne ""} {
puts stderr "whatis: loaded package '$loaded' but no command '$name' appeared"
}
}
if {$resolved eq ""} {
puts stderr "whatis: command '$name' not found (and no matching package could be loaded)"
exit 1
}
out command [join $names { }]
set docid $resolved
if {[llength $subwords]} {
# subcommand-chain resolution (ensembles, aliases, nested chains)
if {[catch {package require punk::ns}]} {
puts stderr "whatis: subcommand chains need punk::ns (punk::ns::cmdinfo) which failed to load"
exit 1
}
if {[catch {punk::ns::cmdinfo {*}$names} cinfo]} {
puts stderr "whatis: punk::ns::cmdinfo failed resolving '[join $names { }]': $cinfo"
exit 1
}
set chain_origin [dict get $cinfo origin]
set args_remaining [dict get $cinfo args_remaining]
set cinfo_docid [dict get $cinfo docid]
out resolves $chain_origin
if {[llength $args_remaining]} {
out warning "trailing words '[join $args_remaining { }]' could not be resolved as subcommands (cmdtype [dict get $cinfo cmdtype])"
}
set resolved [uplevel #0 [list namespace which -command $chain_origin]]
if {$resolved eq ""} {
# documentation-only level (cmdtype doconly) or unresolvable - report what we know
set resolved $chain_origin
}
set docid [expr {$cinfo_docid ne "" ? $cinfo_docid : $resolved}]
} else {
# alias chain (repl aliases are usually registered without leading colons)
set target $resolved
for {set depth 0} {$depth < 5} {incr depth} {
set atarget {}
foreach token [list [string trimleft $target :] $target] {
set atarget [interp alias {} $token]
if {[llength $atarget]} break
}
if {![llength $atarget]} break
out alias "-> $atarget"
set target [uplevel #0 [list namespace which -command [lindex $atarget 0]]]
if {$target eq ""} break
}
if {$target ne "" && $target ne $resolved} {
out resolves $target
set resolved $target
}
set docid $resolved
# prefer cmdinfo's docid when available (handles curried aliases, doc-only ids)
if {![catch {package require punk::ns}] && ![catch {punk::ns::cmdinfo $name} cinfo]} {
set cinfo_docid [dict get $cinfo docid]
if {$cinfo_docid ne ""} {
set docid $cinfo_docid
}
}
}
if {[info procs $resolved] ne ""} {
set kind proc
} elseif {[namespace ensemble exists $resolved]} {
set kind ensemble
} else {
set kind "native command (C-implemented or unavailable as script)"
}
out kind $kind
# ---------------------------------------------------------------- package/origin
set pkginfo [pkg_for_command $resolved]
set origin ""
if {[llength $pkginfo]} {
lassign $pkginfo pkg ver
out package "$pkg $ver"
set origin [pkg_origin $pkg $ver]
if {$origin ne ""} {
out origin $origin
if {[string match //zipfs:* $origin]} {
out warning "origin is the kit-stamped copy inside the executable - relaunch with 'punk91 src script ...' to introspect the working tree under src/modules"
}
}
} else {
out package "(none matched - Tcl core or statically provided)"
}
set defline 0
if {$kind eq "proc"} {
set lines [find_def_lines $origin $resolved]
if {[llength $lines] == 1} {
set defline [lindex $lines 0]
out def $origin:$defline
} elseif {[llength $lines] > 1} {
out def "ambiguous - candidate lines [join $lines {, }] in $origin"
} elseif {$origin ne ""} {
out def "(no proc definition found in origin file - possibly generated at runtime)"
}
}
if {$kind eq "ensemble"} {
set ns [namespace ensemble configure $resolved -namespace]
set map [namespace ensemble configure $resolved -map]
set subs [namespace ensemble configure $resolved -subcommands]
out namespace $ns
if {[dict size $map]} {
puts "map:"
dict for {sub tgt} $map {
puts [format " %-20s -> %s" $sub $tgt]
}
} elseif {[llength $subs]} {
out subcommands $subs
} else {
out subcommands "(exported commands of $ns: [join [info commands ${ns}::*] { }])"
}
}
# ---------------------------------------------------------------- synopsis
if {![catch {package require punk::args}]} {
if {[catch {punk::args::synopsis $docid} syn]} {
set syn ""
}
if {[string trim [strip $syn]] ne ""} {
puts "synopsis:"
foreach line [split [string trim [strip $syn]] \n] {
puts " $line"
}
} else {
puts "synopsis: (no punk::args definition registered for $docid)"
}
}
# ---------------------------------------------------------------- optional body
if {$opt_body && $kind eq "proc"} {
set argspec {}
foreach arg [info args $resolved] {
if {[info default $resolved $arg dflt]} {
lappend argspec [list $arg $dflt]
} else {
lappend argspec $arg
}
}
puts ""
if {$defline} {
puts "body (line numbers are file-relative to $origin):"
} else {
puts "body (line numbers relative to definition - file location unknown):"
}
set bodylines [split [info body $resolved] \n]
puts [format "%5d proc %s \{%s\} \{" $defline $resolved [join $argspec]]
set ln $defline
foreach bline [lrange $bodylines 1 end] {
incr ln
puts [format "%5d %s" $ln $bline]
}
} elseif {$opt_body} {
puts "\nbody: (not a proc - no script body available)"
}
# ---------------------------------------------------------------- optional full doc
if {$opt_doc} {
puts ""
set gotdoc 0
if {![catch {package require punk::ns}]} {
# -return text: plain text, ANSI-free, all forms (punk::ns >= 0.9.0)
if {![catch {punk::ns::cmdhelp -return text -- {*}$names} doctext]} {
set gotdoc 1
}
}
if {!$gotdoc} {
# older punk::ns without -return text (e.g kit-stamped copy) - fall back
if {[info commands ::i] ne ""} {
set doctext [strip [i {*}$names]]
} elseif {![catch {punk::args::usage $docid} usage]} {
set doctext [strip $usage]
} else {
set doctext "doc: (no argument documentation available for $docid)"
}
}
puts $doctext
}
exit 0

98
src/modules/punk/args-999999.0a1.0.tm

@ -5246,7 +5246,11 @@ tcl::namespace::eval punk::args {
# use basic colours here to support terminals without extended colours
#todo - add checks column (e.g -minsize -maxsize)
set errmsg $msg
#2026-07-18 (user direction): the supplied error message (and any per-form
#match detail it carries) renders once, BELOW the usage output - see the
#bottom-append near the end of this proc. It previously also rendered here
#at the top, duplicating the block above and below the usage display.
set errmsg ""
if {![catch {package require textblock}]} {
set has_textblock 1
} else {
@ -5260,14 +5264,8 @@ tcl::namespace::eval punk::args {
}
set errlines [list] ;#for non-textblock output
if {[catch {
if {$use_table} {
append errmsg \n
} else {
if {!$has_textblock && ($returntype in {table tableobject})} {
append errmsg \n "$CLR(errormsg)(layout package textblock is missing)$RST" \n
} else {
append errmsg \n
}
if {!$use_table && !$has_textblock && ($returntype in {table tableobject})} {
append errmsg "$CLR(errormsg)(layout package textblock is missing)$RST" \n
}
set cmdname [Dict_getdef $spec_dict cmd_info -name ""]
set cmdsummary [Dict_getdef $spec_dict cmd_info -summary ""]
@ -6031,12 +6029,35 @@ tcl::namespace::eval punk::args {
$choicetableobj configure -title $CLR(groupname)$groupname
#append help \n[textblock::join -- " " [$choicetableobj print]]
} else {
#string renderer (2026-07-18): markdown-subset list structure so
#choices are machine-distinguishable from their label text (the
#previous flat join left choice names and multi-line labels at
#inconsistent depths with nothing marking which lines were
#choices once ANSI is stripped). Each choice renders as a
#'- `choice`' bullet at a fixed indent; the label's first line
#follows on the bullet line after the closing backtick, label
#continuation lines indent 2 past the bullet dash. The backticked
#token may carry a trailing choiceinfo marker glyph.
if {$groupname ne ""} {
append help \n \n "$CLR(groupname)Group: $groupname$RST"
} else {
append help \n
append help \n " $CLR(groupname)Group '$groupname':$RST"
}
foreach cformatted $formatted {
set clines [split $cformatted \n]
set bulletline " - `[lindex $clines 0]`"
set lbllines [lrange $clines 1 end]
if {[llength $lbllines]} {
set lblfirst [lindex $lbllines 0]
if {[string trim $lblfirst] ne ""} {
append bulletline " - [string trimleft $lblfirst]"
}
append help \n $bulletline
foreach lblline [lrange $lbllines 1 end] {
append help \n " $lblline"
}
} else {
append help \n $bulletline
}
}
append help \n [join $formatted \n]
}
} else {
#we were given an empty set of choices.
@ -6159,8 +6180,26 @@ tcl::namespace::eval punk::args {
$t configure_row [expr {[$t row_count]-1}] -ansibase $A_GOODARG
}
} else {
#review - formatting will be all over the shop due to newlines in typesshow, help
set linetail " TYPE:$typeshow DEFAULT:$default MULTI:$multiple"
#single-line field layout (2026-07-18): the table-cell fields are
#multi-line (type constraints such as -minsize each on their own
#line) - flatten them so each argument renders as one 'name
#TYPE:... ?DEFAULT:...? ?MULTI:yes?' line, and omit empty
#DEFAULT/MULTI rather than rendering empty labels. Constraint
#annotations attach to the type in parentheses e.g
#TYPE:list(-minsize 2 -maxsize 2).
set typeline [string map [list \n " "] $typeshow]
if {$typeline ne $typeshow} {
#had constraint lines - parenthesize them onto the base type
set tparts [split $typeshow \n]
set typeline "[lindex $tparts 0]([join [lrange $tparts 1 end] { }])"
}
set linetail " TYPE:$typeline"
if {[dict exists $arginfo -default]} {
append linetail " DEFAULT:[string map [list \n " "] $default]"
}
if {$is_multiple} {
append linetail " MULTI:yes"
}
if {$hint ne ""} {
set arghelp [textblock::join -- "[a+ bold]$argshow\n$hint$RST" $linetail]
} else {
@ -6172,8 +6211,10 @@ tcl::namespace::eval punk::args {
} elseif {$arg_status eq "ok"} {
set arghelp [punk::ansi::ansiwrap -rawansi $A_GOODARG $arghelp]
}
foreach ln [split $help \n] {
append arghelp " $ln" \n
if {[string trim [punk::ansi::ansistripraw $help]] ne ""} {
foreach ln [split $help \n] {
append arghelp " $ln" \n
}
}
lappend errlines $arghelp
}
@ -6225,7 +6266,11 @@ tcl::namespace::eval punk::args {
if {$use_table} {
$t add_row [list "$argshow" $tp $default "" ""]
} else {
set arghelp "[a+ bold]$argshow$RST TYPE:$tp DEFAULT:$default\n"
set arghelp "[a+ bold]$argshow$RST TYPE:$tp"
if {$default ne ""} {
append arghelp " DEFAULT:$default"
}
append arghelp \n
lappend errlines $arghelp
}
}
@ -6246,7 +6291,11 @@ tcl::namespace::eval punk::args {
if {$use_table} {
$t add_row [list "$argshow" $tp $default "" ""]
} else {
set arghelp "[a+ bold]$argshow$RST TYPE:$tp DEFAULT:$default\n"
set arghelp "[a+ bold]$argshow$RST TYPE:$tp"
if {$default ne ""} {
append arghelp " DEFAULT:$default"
}
append arghelp \n
lappend errlines $arghelp
}
}
@ -6290,12 +6339,17 @@ tcl::namespace::eval punk::args {
set result $t
}
} else {
#put original error at bottom of table too
append result \n $msg
#sole emission of the original error message - below the usage table
if {$msg ne ""} {
append result \n $msg
}
}
} else {
set result $errmsg
append result \n $msg
#sole emission of the original error message - below the usage text
if {$msg ne ""} {
append result \n $msg
}
}
if {$as_error} {
#add PUNK to the tail end of the more usual -errorcode {TCL WRONGARGS} so we maintain reasonable compat with things looking for TCL WRONGARGS - but also differentiate it.

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

@ -1,6 +1,7 @@
0.12.5
0.12.6
#First line must be a semantic version number
#all other lines are ignored.
#0.12.6 - arg_error error-message placement (user direction 2026-07-18): the supplied error message and any per-form match detail it carries ("Bad arguments ... Candidate forms ..." + per-form failure lines) now render exactly once, BELOW the usage display, for both the table and string return forms. Previously the block rendered above the usage AND was appended again below it (the historical "put original error at bottom of table too"), duplicating potentially long multi-form detail. The bottom append is also now suppressed when the message is empty (usage-only calls no longer gain a trailing newline + empty line). tableobject returns are unchanged (they never carried the message). Also (same user direction, format explicitly not frozen - no external consumers yet): the string renderer's argument rows are now single-line 'name TYPE:... ?DEFAULT:...? ?MULTI:yes?' - the multi-line table-cell type constraints (-minsize/-maxsize/-range(s), previously line-broken mid-row so DEFAULT:/MULTI: landed glued to the last constraint line, e.g 'i -return string dict filter' arg vars) parenthesize onto the base type as TYPE:list(-minsize 2 -maxsize 2), empty DEFAULT/MULTI fields are omitted instead of rendering empty labels (MULTI shows 'yes' rather than the check glyph), and whitespace-only help blocks no longer emit indented filler lines. The <unnamed>/arbitrary-flag indicator rows get the same DEFAULT-omission. Choices also gain structure in the string renderer (previously a flat join left choice names and multi-line choicelabels at inconsistent depths, machine-indistinguishable once ANSI is stripped - only the table/ANSI prefix highlighting disambiguated): each choice renders as a markdown-subset bullet '- `choice`' at fixed indent 6 (the backticked token may carry a trailing choiceinfo marker glyph), the label's first line follows the closing backtick on the bullet line after ' - ', label continuations indent to 8 (2 past the bullet dash), and groups head their bullets as "Group 'name':". The resulting structural contract for parsers: column-0 lines bearing TYPE: are argument rows, indented lines are documentation, '- `...`' bullets are choices. Table cells unchanged (construction shared, restructure applied at the string-path consumption site). Feeds punk::ns 0.9.0 cmdhelp -return text (LLM-oriented plain text). Pinned in rendering.test (rendering_stringmode_singleline_argrows, rendering_stringmode_choice_bullets); errorselection.test message pins gained leading * (message no longer starts the raised string); cmdhelp.test message pins sample the last line. No API change.
#0.12.5 - eg usability: punk::args::eg resolves relative/unqualified command names (via punk::ns::cmdinfo from the calling context when punk::ns is loaded - handles subcommand words too - else global qualification). Rationale: in a namespace where punk::args::eg shadows the global 'eg' alias (e.g whilst in ::punk::args itself) 'eg list' hit the raw-id API and returned empty while 'i list' resolved - Tcl name resolution, not an alias deficiency. Unknown ids now return a message naming the id instead of empty; punk::ns::eg hands the typed name through when resolution yields no docid so the message can name it. The usage-table 'Example: eg <id>' header row (the short indication an @examples block exists) now uses scheme colour CLR(check) instead of hardcoded white - the raw id is kept (not @cmd -name, which is display text and not always invocable e.g 'Autoloading Built-in: parray'); a frame-embedded bottom-right marker awaits textblock table footer-block support (candidate goal). New examples.test pins: relative-name resolution, unknown-id message.
#0.12.5 - synopsis notation documented + internal @form -synopsis overrides retired. ::punk::args::parse and ::parse_status dropped their @form -synopsis overrides - they had gone stale (omitted -cache/-caller) and their only added value (the -form member hint) is now expressed properly via -typesynopsis {<int|formname>...} on the -form option; the auto-generated lines stay current as options are added (drift pin: synopsis.test synopsis_parse_generated). @form -synopsis remains fully supported for documentors: define's directive help now documents that it is display-only text needing no Tcl-list validity, that display helpers substitute the command head, and the known warranted case (script-level/constructed ids rendering the raw id - the G-030 lesson recorded in src/modules/AGENTS.md; head-render preference for @cmd -name remains an open generation deficiency). ::punk::args::synopsis -help gains a notation legend ([...] optional, italic argname/<type> placeholders, unitalicised literals, (a|b) alternate grouping to disambiguate from -f|--file alias pipes, {a b} multi-word literal protection, --opt=<type>, text*/*text) and a new @examples block comparing Tcl man-page ?...? style (ambiguous when nested - same delimiter both ends) with the bracket style, viewable via punk::args::eg. New synopsis.test pins: non-list custom -synopsis passthrough + ns head-replacement survival, parse generated-line drift pin, examples block presence.
#0.12.5 - parse -cache overhaul. (1) The VALIDATION trap's parse_cache write is now explicitly gated on -cache - previously it was unconditional but only 'worked' by scoping accident ('variable parse_cache' was declared solely on the -cache 1 branch, so -cache 0 failures wrote to a discarded proc-local). (2) Cached validation errors are now stored as per {errorstyle callertext} render variants under a main key of {arglist definition form} (-caller removed from the main key - results don't depend on it): previously the rendered message baked in the first caller's errorstyle and %caller% attribution and replayed them verbatim to later callers wanting a different style/caller (e.g. a minimal-style render replayed to a -errorstyle standard call). A missing variant falls through to a fresh parse whose trap renders and caches it. (3) @dynamic definitions (and legacy leading '-dynamic 1') are never cached - parse -cache 1 is ignored for them so re-evaluated substitutions always take effect (dev/dynamic-cache.test known-bug pin flipped). (4) New punk::args::parse_cache convenience proc (view summary/keys/dict, -action clear - sgr_cache style); parse_cache variable init now guarded with info exists like the sibling caches; parse's -errorstyle definition default corrected to 'standard' matching the operative defaultopts (doc previously claimed enhanced). New testsuite parsecache.test.

73
src/modules/punk/ns-999999.0a1.0.tm

@ -5386,11 +5386,12 @@ y" {return quirkykeyscript}
usage information as this is inherently risky. (could create a file,
exit the interp etc)
"
-return -type string -default table -choices {string table tableobject dict} -choicelabels {
-return -type string -default table -choices {string table tableobject dict text} -choicelabels {
string "no table layout"
table "full table layout"
tableobject "table object cmd"
dict "machine-parsable resolution + parse-status"
text "plain text, ANSI-free, all forms"
} -help\
"Return form of the usage information.
'dict' returns a dict with keys origin, docid, cmdtype,
@ -5399,7 +5400,13 @@ y" {return quirkykeyscript}
the resolved definition (see punk::args::parse_status for the
documented structure; empty if the command is undocumented).
Its per-argument statuses distinguish a fully-valid, an invalid
and an incomplete argument set machine-parsably."
and an incomplete argument set machine-parsably.
'text' returns plain text with no ANSI codes and no table
layout, rendering the argument section of every command form
(or of each form in an explicit -form selection) in a single
output - unlike the interactive displays, which render only
the best-matching form's argument table. Intended for
machine/LLM consumption of the full documentation."
} {${[punk::args::resolved_def -types opts ::punk::args::arg_error -scheme]}} {
@ -5468,6 +5475,68 @@ y" {return quirkykeyscript}
set nextopts [dict remove $opts -grepstr]
#JJJ
if {$opt_return eq "text"} {
#Plain-text renderer for machine/LLM consumption: no ANSI, no table layout,
#and the argument section of every selected command form rendered in one
#output rather than only the G-041 best-matching form's argument table.
#Resolution (aliases, ensembles, docid, form candidacy) is delegated to the
#-return dict recursion; each form's argument section then renders via the
#string renderer and the sections are joined below the common header.
set dinfo [uplevel 1 [list ::punk::ns::cmdhelp -return dict -form $opt_form -- $querycommand {*}$queryargs]]
set docid [dict get $dinfo docid]
if {$docid eq ""} {
return "Undocumented command [dict get $dinfo origin]. Type: [dict get $dinfo cmdtype]"
}
set spec [punk::args::get_spec $docid]
set pstatus [dict get $dinfo parsestatus]
if {$opt_form ne "*"} {
set forms $opt_form
} elseif {[llength [dict get $dinfo args_remaining]]} {
#argument words were supplied - restrict to the form(s) the advisory
#parse selected (matched/best-candidate; all candidates on noformmatch)
if {![dict get $pstatus ok] && [dict get $pstatus failureclass] in {noformmatch multipleformmatches}} {
set forms [dict keys [dict get $pstatus formstatus]]
} else {
set forms [dict get $pstatus form]
}
} else {
set forms [dict get $spec form_names]
}
if {[llength [dict get $spec form_names]] < 2} {
set result [punk::ansi::ansistripraw [punk::args::arg_error "" $spec -aserror 0 -return string -scheme nocolour -form [lindex $forms 0]]]
} else {
set marker " --ARGUMENTS-- " ;#the string renderer's argument-section heading line
set result ""
set fnum 0
foreach fid $forms {
set fout [punk::ansi::ansistripraw [punk::args::arg_error "" $spec -aserror 0 -return string -scheme nocolour -form $fid]]
set idx [string first $marker $fout]
if {$idx < 0} {
#no argument-section heading (e.g custom FORMDISPLAY) - emit whole output once
if {$fnum == 0} {
append result $fout
}
} else {
if {$fnum == 0} {
#common header (command help + all-forms synopsis) - emit once
append result [string range $fout 0 $idx-1]
}
append result " --ARGUMENTS (form '$fid')-- " [string range $fout $idx+[string length $marker] end] \n
}
incr fnum
}
set result [string trimright $result \n]
}
if {$opt_grepstr ne ""} {
if {[llength $opt_grepstr] == 1} {
set result [punk::ansi::grepstr --ignore-case -return all [lindex $opt_grepstr 0] $result]
} else {
set result [punk::ansi::grepstr --ignore-case -return all -highlight [lrange $opt_grepstr 1 end] [lindex $opt_grepstr 0] $result]
}
}
return $result
}
set whichinfo [uplevel 1 [list ::punk::ns::cmdwhich $querycommand]]
set rootorigin [dict get $whichinfo origin]
set which [dict get $whichinfo which]

3
src/modules/punk/ns-buildversion.txt

@ -1,6 +1,7 @@
0.8.1
0.9.0
#First line must be a semantic version number
#all other lines are ignored.
#0.9.0 - cmdhelp -return text: plain-text renderer for machine/LLM consumption - no ANSI codes, no table layout, and the argument section of EVERY command form (or of each form in an explicit -form selection) rendered in a single output, unlike the interactive displays which render only the G-041 best-matching form's argument table ('i -return text after' shows all six after forms' argument sections under one common header). Implementation: resolution is delegated to the existing -return dict recursion (aliases/ensembles/docid/form candidacy all inherited); each form's argument section renders via arg_error -return string -scheme nocolour restricted with -form, is ansistripraw'd, and multiform outputs join the per-form sections (headed ' --ARGUMENTS (form <name>)-- ') below the common header+synopsis emitted once. Argument words still restrict the rendered forms per G-041 semantics ('i -return text after cancel xx' renders the matched cancelscript form only); no advisory-parse error preamble is included (the text form is documentation, not validation - use -return dict for parse status). -grepstr applies to the final text as for other return forms. Supported by the punk::args 0.12.6 arg_error changes (message below usage once; single-line 'name TYPE:... ?DEFAULT:...? ?MULTI:yes?' argument rows in the string renderer). Tests: cmdhelp.test cmdhelp_return_text_all_forms / _argword_form_selection / _single_form_plain_marker.
#0.8.1 - synopsis: fixed 'list element in braces followed by "?" instead of space' error when rendering a command whose definition carries a custom @form -synopsis that is not a valid Tcl list (e.g Tcl manpage style '?-form {int|<formname>...}?' as ::punk::args::parse formerly used - 's parse' errored). The command-head replacement treated each synopsis line as a Tcl list (lreplace); non-list lines now take a textual fallback that replaces the leading word(s) via index walking and passes the tail through verbatim. Pinned in punk/args synopsis.test (synopsis_form_custom_override_nonlist) since that suite already exercises the punk::ns::synopsis path.
#0.8.0 - removed the global command aliases nscommands1 and nscommands2 (user direction 2026-07-14, flagged as removal candidates in the 0.7.0 hygiene pass): earlier pipeline-based iterations superseded by the plain proc nscommands (which adds weird-namespace handling). Neither was documented/exported and no callers existed; nscommands1 was broken - it referenced nonexistent commands (nsthis2, inspect at global scope) and errored on first use. Recoverable via git history.
#0.7.1 - hygiene-pass backlog fixes (both pinned before the fix, pins flipped with it): (a) get_nslist's -types default was the literal string $known_types (braced-literal defaults bug) so any call without -types - notably bare user-level 'nslist' - errored with a malformed 'Unrecognised namespace member type: $known_types'; an unsupplied -types now defaults to all known member types (nslist.test nslist_types_default; nslist's own PUNKARGS -types gained help text and deliberately no -default so the passthrough stays absent when unsupplied). (b) nsjoinall's non-empty-prefix-vs-absolute-path error message carried a copy-paste 'nsjoin:' prefix - now 'nsjoinall:' (nsprimitives.test nsjoinall_edges). Also commented out a development trace puts ('>>> base: ...') that polluted nscommands stdout on the weird-namespace path (same treatment as the 0.1.1 trace-puts cleanup).

13
src/tests/modules/punk/args/testsuites/args/errorselection.test

@ -10,6 +10,9 @@ package require tcltest
#original _GAP pins encoded that pre-fix behaviour; they were flipped 2026-07-14
#(agent, G-082) to the pointed choiceviolation/typemismatch expectations when the
#allocation-rejection capture + overflow-time error selection shipped.
#2026-07-18 (agent): message-content string match patterns gained a leading *:
#punk::args 0.12.6 renders the error message once BELOW the usage display (was
#above and below), so the message no longer starts the raised result string.
namespace eval ::testspace {
namespace import ::tcltest::*
variable common {
@ -38,7 +41,7 @@ namespace eval ::testspace {
set err [catch {punk::args::parse {frobnicate} withid $docid} msg opts]
lappend result $err {*}[ecinfo $opts]
lappend result [string match "Leading argument 'subcommand' for*must be one of the listed values:*Received: 'frobnicate'*" $msg]
lappend result [string match "*Leading argument 'subcommand' for*must be one of the listed values:*Received: 'frobnicate'*" $msg]
}\
-cleanup {
foreach id $docids {
@ -60,7 +63,7 @@ namespace eval ::testspace {
set err [catch {punk::args::parse {frobnicate} withid $docid} msg opts]
lappend result $err {*}[ecinfo $opts]
lappend result [string match "Trailing argument 'subject' for*must be one of the listed values:*Received: 'frobnicate'*" $msg]
lappend result [string match "*Trailing argument 'subject' for*must be one of the listed values:*Received: 'frobnicate'*" $msg]
}\
-cleanup {
foreach id $docids {
@ -84,7 +87,7 @@ namespace eval ::testspace {
set err [catch {punk::args::parse {notanumber} withid $docid} msg opts]
lappend result $err {*}[ecinfo $opts]
lappend result [string match "Trailing argument 'num' for*requires type 'int'. Received: 'notanumber'*" $msg]
lappend result [string match "*Trailing argument 'num' for*requires type 'int'. Received: 'notanumber'*" $msg]
}\
-cleanup {
foreach id $docids {
@ -106,11 +109,11 @@ namespace eval ::testspace {
set err [catch {punk::args::parse {frobnicate} withid $docid1} msg opts]
lappend result $err {*}[ecinfo $opts]
lappend result [string match "Leading argument 'subcommand' for*must be one of the listed values:*Received: 'frobnicate'*" $msg]
lappend result [string match "*Leading argument 'subcommand' for*must be one of the listed values:*Received: 'frobnicate'*" $msg]
set err [catch {punk::args::parse {frobnicate} withid $docid2} msg opts]
lappend result $err {*}[ecinfo $opts]
lappend result [string match "Trailing argument 'subject' for*must be one of the listed values:*Received: 'frobnicate'*" $msg]
lappend result [string match "*Trailing argument 'subject' for*must be one of the listed values:*Received: 'frobnicate'*" $msg]
}\
-cleanup {
foreach id $docids {

73
src/tests/modules/punk/args/testsuites/args/rendering.test

@ -616,5 +616,78 @@ GFLUSH line"
rename ::testspace::rdyn_artlabels ""
}\
-result [list 1 1 1]
#--- -return string single-line argument rows (added 2026-07-18, punk::args 0.12.6) --------
#The string renderer emits one 'name TYPE:... ?DEFAULT:...? ?MULTI:yes?' line per
#argument: the multi-line table-cell type constraints (-minsize etc) parenthesize onto
#the base type, empty DEFAULT/MULTI fields are omitted rather than rendering empty
#labels, and whitespace-only help blocks emit no indented filler lines.
test rendering_stringmode_singleline_argrows {string renderer: one line per argument - constraints parenthesized, empty DEFAULT/MULTI omitted}\
-setup $common -body {
punk::args::define {
@id -id ::testspace::rstrrow
@cmd -name testspace::rstrrow -summary "rstrrow fixture." -help "cmd help."
@opts
-flagm -type string -multiple 1 -default fmv -help "flagm help."
@values -min 1 -max 2
sized -type list -minsize 2 -maxsize 2 -help "sized help."
plain -type string -optional 1
}
set out [punk::ansi::ansistrip [punk::args::usage -return string ::testspace::rstrrow]]
#argument rows start at column 0 - indented lines are help/choice content
foreach ln [split $out \n] {
if {[string match "sized *" $ln]} {
lappend result [string match "sized TYPE:list(-minsize 2 -maxsize 2)" [string trimright $ln]]
}
if {[string match "?-flagm*" $ln]} {
lappend result [string match "*TYPE:string DEFAULT:'fmv' MULTI:yes" [string trimright $ln]]
}
if {[string match "?plain?*" $ln]} {
#no default, not multiple - no empty DEFAULT:/MULTI: labels rendered
lappend result [string match "*DEFAULT*" $ln] [string match "*MULTI*" $ln]
}
}
set result
}\
-cleanup {
punk::args::undefine ::testspace::rstrrow 1
}\
-result [list 1 1 0 0]
test rendering_stringmode_choice_bullets {string renderer: choices render as backticked bullet items - labels distinguishable from choice names without ANSI}\
-setup $common -body {
punk::args::define {
@id -id ::testspace::rstrchoice
@cmd -name testspace::rstrchoice -summary "rstrchoice fixture." -help "cmd help."
@values -min 1 -max 1
mode -type string -choices {fast careful} -choicelabels {
fast "quick but
approximate"
careful "slow and thorough"
} -help "mode help."
}
set out [punk::ansi::ansistrip [punk::args::usage -return string ::testspace::rstrchoice]]
foreach ln [split $out \n] {
set rtrimmed [string trimright $ln]
if {[string match {*- `fast`*} $ln]} {
#bullet at fixed indent, label first line after the closing backtick
lappend result [string match { - `fast` - quick but} $rtrimmed]
}
if {[string match {*approximate*} $ln]} {
#label continuation lines indent past the bullet dash
lappend result [string match { *approximate} $rtrimmed]
}
if {[string match {*- `careful`*} $ln]} {
#single-line label rides the bullet line entirely
lappend result [string match { - `careful` - slow and thorough} $rtrimmed]
}
}
set result
}\
-cleanup {
punk::args::undefine ::testspace::rstrchoice 1
}\
-result [list 1 1 1]
}
tcltest::cleanupTests ;#needed to produce test summary line.

59
src/tests/modules/punk/ns/testsuites/ns/cmdhelp.test

@ -241,7 +241,8 @@ namespace eval ::testspace {
set out [punk::ns::cmdhelp ::testspace::helpfix v1]
lappend result [has_sgr_with $out $INFOBORDER]
#message wording varies by which check fails (count vs allocation) - both start "Bad number of"
lappend result [string match "Bad number of*" [punk::ansi::ansistrip [lindex [split $out \n] 0]]]
#punk::args 0.12.6: the message renders once BELOW the usage display - sample the last line
lappend result [string match "Bad number of*" [punk::ansi::ansistrip [lindex [split $out \n] end]]]
}\
-cleanup {
}\
@ -344,7 +345,8 @@ namespace eval ::testspace {
variable BAD
set out [punk::ns::cmdhelp ::testspace::helpfix v1 x x]
#the message names the failing argument and its row is badarg-marked
lappend result [string match "*fail on firstval*" [punk::ansi::ansistrip [lindex [split $out \n] 0]]]
#punk::args 0.12.6: the message renders once BELOW the usage display - sample the last line
lappend result [string match "*fail on firstval*" [punk::ansi::ansistrip [lindex [split $out \n] end]]]
lappend result [any_line_has_sgr $out firstval $BAD]
lappend result [any_line_has_sgr $out lastval $BAD]
}\
@ -365,14 +367,15 @@ namespace eval ::testspace {
test cmdhelp_errormsg_names_queried_command {the parse-failure message names the queried command at any invocation depth - never cmdhelp's internal parse source}\
-setup $common -body {
#top-level call - the interactive 'i <cmd>' case
#punk::args 0.12.6: the message renders once BELOW the usage display - sample the last line
set out [uplevel #0 [list punk::ns::cmdhelp ::testspace::helpfix v1]]
set line1 [punk::ansi::ansistrip [lindex [split $out \n] 0]]
lappend result [string match {*punk::args::parse*} $line1]
lappend result [string match {*::testspace::helpfix*} $line1]
set msgline [punk::ansi::ansistrip [lindex [split $out \n] end]]
lappend result [string match {*punk::args::parse*} $msgline]
lappend result [string match {*::testspace::helpfix*} $msgline]
#nested context attributes identically
set out [punk::ns::cmdhelp ::testspace::helpfix v1]
set line1 [punk::ansi::ansistrip [lindex [split $out \n] 0]]
lappend result [string match {*::testspace::helpfix*} $line1]
set msgline [punk::ansi::ansistrip [lindex [split $out \n] end]]
lappend result [string match {*::testspace::helpfix*} $msgline]
}\
-cleanup {
}\
@ -736,5 +739,47 @@ namespace eval ::testspace {
-cleanup {
}\
-result [list 0]
#--- -return text (ns 0.9.0 plain-text all-forms renderer) ---------------------------------
#added 2026-07-18 (agent): machine/LLM-oriented plain text - no ANSI, no table layout,
#every form's argument section under one common header, no advisory-parse error preamble.
test cmdhelp_return_text_all_forms {-return text renders every form's argument section in one ANSI-free output with the common header once}\
-setup $common -body {
set out [punk::ns::cmdhelp -return text ::testspace::formfix]
lappend result [expr {[string first \x1b $out] < 0}]
foreach fname {ms cancel idle} {
lappend result [string match "*--ARGUMENTS (form '$fname')--*" $out]
}
#the @cmd -help text renders once (common header emitted only for the first form)
lappend result [regexp -all {formfix multiform} $out]
#documentation render - no advisory-parse failure preamble
lappend result [string match "*No form of the command matches*" $out]
}\
-cleanup {
}\
-result [list 1 1 1 1 1 0]
test cmdhelp_return_text_argword_form_selection {-return text with argument words renders only the matched form's argument section (G-041 parity)}\
-setup $common -body {
set out [punk::ns::cmdhelp -return text ::testspace::formfix cancel someid]
lappend result [string match "*--ARGUMENTS (form 'cancel')--*" $out]
lappend result [string match "*--ARGUMENTS (form 'ms')--*" $out]
lappend result [string match "*--ARGUMENTS (form 'idle')--*" $out]
}\
-cleanup {
}\
-result [list 1 0 0]
test cmdhelp_return_text_single_form_plain_marker {-return text on a single-form command keeps the plain argument-section heading (no form label)}\
-setup $common -body {
set out [punk::ns::cmdhelp -return text ::testspace::helpfix]
lappend result [expr {[string first \x1b $out] < 0}]
lappend result [string match "* --ARGUMENTS-- *" $out]
lappend result [string match "*--ARGUMENTS (form*" $out]
}\
-cleanup {
}\
-result [list 1 1 0]
}
tcltest::cleanupTests ;#needed to produce test summary line.

Loading…
Cancel
Save