Browse Source

agent skills: tcl-nslist namespace discovery; punk::ns 0.9.1 packagetails fix

New scriptlib/developer/nslist.tcl backing the new cross-framework tcl-nslist
agent skill (.agents/skills/ canonical, .claude/skills/ synced copy): the
discovery counterpart of whatis.tcl - lists what a namespace/package actually
exposes at runtime via punk::ns::get_ns_dicts (the n// machinery): child
namespaces with loadable-package markers, commands classified by kind,
punk::args-documented commands, -synopsis whole-namespace usage lines,
-pathcommands namespace-path resolution. Auto-package-requires unloaded
packages and follows ensemble commands to their implementation namespace.
The tcl-whatis skill/script cross-reference it (whatis redirects with a hint
when the queried name is a namespace, not a command).

punk::ns 0.9.1: get_ns_dicts packagetails/packageprefixes initialization moved
inside the per-namespace loop - with multiple matched namespaces (path globs,
trailing **) every dict after the first carried the union of all earlier
namespaces' entries, mis-marking non-package children in nslist/n// underline
styling and in the returned data; a child with several deeper packages below
it was appended to packageprefixes once per matching package (now once).
Pinned in nslist.test (nslist_classification_multins_package_buckets_reset).
Documentation-only PUNKARGS added for get_ns_dicts (returned dict keys
documented) and nslist_dict; parsing stays manual.

Root AGENTS.md: introspection note + scriptlib/developer inventory updated.
Project version 0.15.1 + CHANGELOG entry. punk::ns suite 102/102 pass.

Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
master
Julian Noble 1 week ago
parent
commit
4fc1d62ce2
  1. 85
      .agents/skills/tcl-nslist/SKILL.md
  2. 9
      .agents/skills/tcl-whatis/SKILL.md
  3. 85
      .claude/skills/tcl-nslist/SKILL.md
  4. 9
      .claude/skills/tcl-whatis/SKILL.md
  5. 4
      AGENTS.md
  6. 5
      CHANGELOG.md
  7. 2
      punkproject.toml
  8. 275
      scriptlib/developer/nslist.tcl
  9. 14
      scriptlib/developer/whatis.tcl
  10. 95
      src/modules/punk/ns-999999.0a1.0.tm
  11. 3
      src/modules/punk/ns-buildversion.txt
  12. 22
      src/tests/modules/punk/ns/testsuites/ns/nslist.test

85
.agents/skills/tcl-nslist/SKILL.md

@ -0,0 +1,85 @@
---
name: tcl-nslist
description: Use when you need to discover what exists in a Tcl namespace or package in this repo - what commands/procs a namespace exposes, whether a helper already exists before writing one, or what an ensemble's implementation namespace contains. No specific command name needed (that's tcl-whatis). Runtime listing via punkshell using punk::ns::get_ns_dicts (the machinery behind the interactive n// browser): child namespaces with loadable-package markers, commands classified by kind (proc/alias/ensemble/native/oo/imported), which commands have punk::args docs, optional whole-namespace usage synopses. Auto-loads the package; sees runtime-generated commands grep cannot.
---
# tcl-nslist
Namespace/package discovery for the punkshell repo - the "what is available?"
counterpart of tcl-whatis ("how do I call X?"). Asks the live interpreter via
punk::ns::get_ns_dicts - the same machinery behind the interactive n// / n///
namespace browsers - so it reports what is actually loaded, including
runtime-generated commands, aliases and imports that grep cannot see, and
auto-loads the providing package when the namespace is not yet populated.
## Command
Run from the repo root (`punk91` lives in `bin/`; any punkshell kit works):
punk91 src script scriptlib/developer/nslist.tcl ?-synopsis? ?-pathcommands? <nsglob>...
- `<nsglob>` - namespace name or glob, qualified or not: `punk::ns` is listed
as `::punk::ns::*`. If the namespace does not exist, `package require` of
the name (then of successively shorter :: prefixes) is attempted first.
Last-segment glob chars filter members (`punk::ns::ns*`); glob chars in
earlier segments match multiple namespaces (`::tcl::*::d*`); a trailing
`::**` segment recurses through all descendant namespaces
(`punk::nav::**`). Quote glob arguments in PowerShell.
An ensemble command name (e.g `string`) is followed to its implementation
namespace, with a note.
- `-synopsis` - append punk::args usage lines for every documented command in
the namespace (a one-call API cheat-sheet; large for big namespaces).
- `-pathcommands` - also list the commands resolvable via each `namespace
path` entry.
Exit codes: 0 every pattern listed something, 1 some pattern matched nothing,
2 usage error.
## Output fields
One block per matched namespace (blank-line separated); empty categories are
omitted:
namespace: ::punk::ns fully-qualified namespace
children: argdoc lib ... child namespace tails
packagetails: lib ... children that are also loadable package names
packageprefixes: ... children with deeper loadable packages below them
commands: 147 total member commands (count)
procs: ... script procs
aliases: ... interp aliases (may lack a same-named command)
ensembles: ...
native: ... C-implemented commands
imported: ... imported from another namespace
other: ... kinds not otherwise classified
exported: ... commands matching the namespace export patterns
documented: ... have punk::args docs - tcl-whatis gives full detail
exportpatterns: {[a-z]*} raw namespace export patterns
namespacepath: ... extra namespaces this ns resolves commands from
note: ... autoload / ensemble-follow / empty-namespace notes
synopsis: with -synopsis: usage line(s) per documented command
ooclasses/ooobjects/ooprivateclasses/ooprivateobjects/coroutines/interps/
zlibstreams lines appear when such commands exist. `packagetails` is the
discovery lever: a child listed there can be `package require`d and listed
itself.
## Caveats
- Keep the `src` launch argument: it loads and queries the working-tree
modules under `src/modules`; without it the kit-stamped copies inside the
executable answer instead.
- Load noise ("src mode: registered ...") goes to stderr; read stdout only
(append `2>/dev/null` in bash, `2>$null` in PowerShell).
- Only what is loaded in that session is reported. The script auto-requires
the queried package, but a namespace can be populated further by other
packages (e.g plugins) - an unexpectedly sparse listing may mean a related
package is not loaded, not that the commands do not exist.
- `documented:` lists commands with registered punk::args definitions;
undocumented commands are still real - tcl-whatis reports their proc
signature and `-body` even without an argdoc.
- From Tcl code, the same data is available structurally:
`punk::ns::get_ns_dicts ::the::ns::* ?-allbelow <bool>? ?-nspathcommands <bool>?`
returns a list of dicts (keys documented - see
`punk91 src script scriptlib/developer/whatis.tcl -doc punk::ns::get_ns_dicts`),
and `punk::ns::nslist_dict` accepts caller-relative globs. In an interactive
punkshell the `n//` / `n///` aliases render the same information.

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

@ -1,6 +1,6 @@
---
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.
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. For namespace/package-level discovery (listing what a namespace exposes, no command name needed) use the tcl-nslist skill.
---
# tcl-whatis
@ -60,6 +60,13 @@ Ensembles report their namespace and subcommand map instead of `def:`.
- Multiform commands render one ` --ARGUMENTS (form 'name')-- ` section per
form under a single common header (description + all-forms synopsis).
## Namespace discovery (sibling skill)
whatis needs a command name. For the discovery question - what commands and
child namespaces a namespace or package exposes at all - use the tcl-nslist
skill (scriptlib/developer/nslist.tcl). whatis prints a redirect hint when the
queried name turns out to be a namespace rather than a command.
## Caveats
- Keep the `src` launch argument: it loads the working-tree modules under

85
.claude/skills/tcl-nslist/SKILL.md

@ -0,0 +1,85 @@
---
name: tcl-nslist
description: Use when you need to discover what exists in a Tcl namespace or package in this repo - what commands/procs a namespace exposes, whether a helper already exists before writing one, or what an ensemble's implementation namespace contains. No specific command name needed (that's tcl-whatis). Runtime listing via punkshell using punk::ns::get_ns_dicts (the machinery behind the interactive n// browser): child namespaces with loadable-package markers, commands classified by kind (proc/alias/ensemble/native/oo/imported), which commands have punk::args docs, optional whole-namespace usage synopses. Auto-loads the package; sees runtime-generated commands grep cannot.
---
# tcl-nslist
Namespace/package discovery for the punkshell repo - the "what is available?"
counterpart of tcl-whatis ("how do I call X?"). Asks the live interpreter via
punk::ns::get_ns_dicts - the same machinery behind the interactive n// / n///
namespace browsers - so it reports what is actually loaded, including
runtime-generated commands, aliases and imports that grep cannot see, and
auto-loads the providing package when the namespace is not yet populated.
## Command
Run from the repo root (`punk91` lives in `bin/`; any punkshell kit works):
punk91 src script scriptlib/developer/nslist.tcl ?-synopsis? ?-pathcommands? <nsglob>...
- `<nsglob>` - namespace name or glob, qualified or not: `punk::ns` is listed
as `::punk::ns::*`. If the namespace does not exist, `package require` of
the name (then of successively shorter :: prefixes) is attempted first.
Last-segment glob chars filter members (`punk::ns::ns*`); glob chars in
earlier segments match multiple namespaces (`::tcl::*::d*`); a trailing
`::**` segment recurses through all descendant namespaces
(`punk::nav::**`). Quote glob arguments in PowerShell.
An ensemble command name (e.g `string`) is followed to its implementation
namespace, with a note.
- `-synopsis` - append punk::args usage lines for every documented command in
the namespace (a one-call API cheat-sheet; large for big namespaces).
- `-pathcommands` - also list the commands resolvable via each `namespace
path` entry.
Exit codes: 0 every pattern listed something, 1 some pattern matched nothing,
2 usage error.
## Output fields
One block per matched namespace (blank-line separated); empty categories are
omitted:
namespace: ::punk::ns fully-qualified namespace
children: argdoc lib ... child namespace tails
packagetails: lib ... children that are also loadable package names
packageprefixes: ... children with deeper loadable packages below them
commands: 147 total member commands (count)
procs: ... script procs
aliases: ... interp aliases (may lack a same-named command)
ensembles: ...
native: ... C-implemented commands
imported: ... imported from another namespace
other: ... kinds not otherwise classified
exported: ... commands matching the namespace export patterns
documented: ... have punk::args docs - tcl-whatis gives full detail
exportpatterns: {[a-z]*} raw namespace export patterns
namespacepath: ... extra namespaces this ns resolves commands from
note: ... autoload / ensemble-follow / empty-namespace notes
synopsis: with -synopsis: usage line(s) per documented command
ooclasses/ooobjects/ooprivateclasses/ooprivateobjects/coroutines/interps/
zlibstreams lines appear when such commands exist. `packagetails` is the
discovery lever: a child listed there can be `package require`d and listed
itself.
## Caveats
- Keep the `src` launch argument: it loads and queries the working-tree
modules under `src/modules`; without it the kit-stamped copies inside the
executable answer instead.
- Load noise ("src mode: registered ...") goes to stderr; read stdout only
(append `2>/dev/null` in bash, `2>$null` in PowerShell).
- Only what is loaded in that session is reported. The script auto-requires
the queried package, but a namespace can be populated further by other
packages (e.g plugins) - an unexpectedly sparse listing may mean a related
package is not loaded, not that the commands do not exist.
- `documented:` lists commands with registered punk::args definitions;
undocumented commands are still real - tcl-whatis reports their proc
signature and `-body` even without an argdoc.
- From Tcl code, the same data is available structurally:
`punk::ns::get_ns_dicts ::the::ns::* ?-allbelow <bool>? ?-nspathcommands <bool>?`
returns a list of dicts (keys documented - see
`punk91 src script scriptlib/developer/whatis.tcl -doc punk::ns::get_ns_dicts`),
and `punk::ns::nslist_dict` accepts caller-relative globs. In an interactive
punkshell the `n//` / `n///` aliases render the same information.

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

@ -1,6 +1,6 @@
---
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.
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. For namespace/package-level discovery (listing what a namespace exposes, no command name needed) use the tcl-nslist skill.
---
# tcl-whatis
@ -60,6 +60,13 @@ Ensembles report their namespace and subcommand map instead of `def:`.
- Multiform commands render one ` --ARGUMENTS (form 'name')-- ` section per
form under a single common header (description + all-forms synopsis).
## Namespace discovery (sibling skill)
whatis needs a command name. For the discovery question - what commands and
child namespaces a namespace or package exposes at all - use the tcl-nslist
skill (scriptlib/developer/nslist.tcl). whatis prints a redirect hint when the
queried name turns out to be a namespace rather than a command.
## Caveats
- Keep the `src` launch argument: it loads the working-tree modules under

4
AGENTS.md

@ -199,7 +199,7 @@ The project version is fully independent of module versions. A module bump (even
- `TEMP_REFERENCE/` - Reference-only material for agents, not a work folder; ignored by both git and fossil, so its contents (including its AGENTS.md) are machine-local and unversioned. Holds the nested fossil checkout of the Tcl core sources at `TEMP_REFERENCE/tcl9`, which must stay on `trunk` per the fossil branch-discipline rule in User Preferences (see TEMP_REFERENCE/AGENTS.md when present)
- 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, `whatis.tcl` runtime command introspection backing the `tcl-whatis` agent skill, `runtests_parity.tcl` comparing `runtests.tcl -report json` outputs for result parity between runner modes per src/tests/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, `nslist.tcl` runtime namespace/package discovery backing the `tcl-nslist` agent skill, `runtests_parity.tcl` comparing `runtests.tcl -report json` outputs for result parity between runner modes per src/tests/AGENTS.md Verification); 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`
@ -213,7 +213,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`.
- 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`. Its discovery counterpart for namespace/package contents (child namespaces with loadable-package markers, commands classified by kind, punk::args-documented commands, `-synopsis` whole-namespace usage lines - via `punk::ns::get_ns_dicts`, the `n//` machinery): `punk91 src script scriptlib/developer/nslist.tcl ?-synopsis? ?-pathcommands? <nsglob>...` - see `.agents/skills/tcl-nslist/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.

5
CHANGELOG.md

@ -5,6 +5,11 @@ 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.15.1] - 2026-07-19
- punk::ns 0.9.1: `get_ns_dicts` per-namespace `packagetails`/`packageprefixes` fix - the two lists were initialized once before the matched-namespaces loop, so with multiple matched namespaces (path globs, trailing `**`) every dict after the first carried the union of all earlier namespaces' entries, mis-marking non-package children in `nslist`/`n//` underline styling and in the returned data; a child with several deeper packages below it was also recorded once per matching package (now once). Single-namespace queries (the nslist.test tier A pins) were unaffected; the multi-namespace reset is now pinned in nslist.test. Documentation-only PUNKARGS added for `get_ns_dicts` (documents the returned per-namespace dict keys and `-allbelow`/`-nspathcommands`) and `nslist_dict`.
- New developer/agent namespace-discovery script `scriptlib/developer/nslist.tcl` backing the new cross-framework `tcl-nslist` agent skill (canonical `.agents/skills/`, synced copy in `.claude/skills/`): the discovery counterpart of `whatis.tcl` - lists what a namespace or package actually exposes at runtime via `punk::ns::get_ns_dicts` (child namespaces with loadable-package markers, commands classified by kind, punk::args-documented commands, `-synopsis` whole-namespace usage lines, `-pathcommands` namespace-path resolution), auto-`package require`s unloaded packages and follows ensemble commands to their implementation namespace. The `tcl-whatis` skill/script now cross-reference it (whatis redirects with a hint when the queried name is a namespace rather than a command).
## [0.15.0] - 2026-07-18
- shellrun 0.2.0: `runx` gains `-teelog` - a live tee of the captured stdout/stderr to the shellfilter::log mechanism (shellthread worker threads; `-tag`/`-syslog host:port`/`-file`/`-raw` settings; log sources `<tag>stdout`/`<tag>stderr`) while the captured/returned data stays unchanged. Implemented as write traces on the capture variables rather than additional channel transforms: probing showed the unaudited float-placement logic can starve a tee_to_log float combined with the float-locked capture junction, and shellthread's parked-worker adoption does not re-apply settings (logs silently misrouted to a previous target) - both findings recorded for a future shellfilter/shellthread audit. Tag workers are left open for reuse across calls; a conflicting reconfiguration errors explicitly.

2
punkproject.toml

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

275
scriptlib/developer/nslist.tcl

@ -0,0 +1,275 @@
#!/usr/bin/env tclsh
# nslist.tcl - runtime namespace/command discovery report, aimed at agent/LLM use
#
# Answers "what does this namespace/package actually expose?" - the discovery
# counterpart of whatis.tcl (which answers "how do I call this command?").
# Reports for each namespace matched by <nsglob>:
# - child namespaces, marking which are also loadable package names
# - commands classified by kind (procs, aliases, ensembles, native, imported,
# oo classes/objects, coroutines, interps, zlibstreams)
# - which commands have punk::args documentation ('documented:' line) - those
# give full usage detail via whatis.tcl / punk::ns::cmdhelp
# - namespace export patterns and 'namespace path' entries
# Data comes from punk::ns::get_ns_dicts - the same runtime machinery behind the
# interactive n// and n/// namespace browsers - so runtime-generated commands are
# included and only what is actually loaded is reported.
# If the target namespace does not exist, 'package require' of the name (then of
# successively shorter :: prefixes) is attempted first to populate it.
# An ensemble command name (e.g 'string') is followed to its implementation
# namespace, with a note.
# Options:
# -synopsis append punk::args usage lines for each documented command
# -pathcommands also list the commands resolvable via each 'namespace path' entry
#
# Must run inside a punkshell interpreter (needs punk::ns, punk::args):
# punk91 src script scriptlib/developer/nslist.tcl ?-synopsis? ?-pathcommands? <nsglob>...
# Use the 'src' launch argument so the working-tree modules under src/modules are
# loaded and queried rather than the kit-stamped copies inside the executable.
# <nsglob> may be a plain namespace (punk::ns - listed as ::punk::ns::*) or a
# glob: last-segment glob chars match members, earlier-segment glob chars match
# multiple namespaces, and a trailing ::** segment recurses through all
# descendant namespaces.
# Exit 0 if every pattern produced a listing; 1 if any pattern matched nothing;
# 2 usage error.
if {[catch {package require punk::ns} errmsg]} {
puts stderr "nslist: requires a punkshell interpreter (package require punk::ns failed: $errmsg)"
exit 1
}
catch {package require punk::args}
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 "%-16s %s" $key: [strip $val]]
}
# attempt to load a package matching $name, else successively shorter :: prefixes
proc try_autoload {name} {
set parts [lsearch -all -inline -not [split [string trimleft $name :] ::] {}]
for {set n [llength $parts]} {$n >= 1} {incr n -1} {
set prefix [join [lrange $parts 0 $n-1] ::]
if {![catch {package require $prefix}]} {
return $prefix
}
}
return {}
}
# leading non-glob namespace of a fully-qualified (possibly glob) path e.g ::a::b* -> ::a
proc nonglob_base {fq} {
set parts [lsearch -all -inline -not [split [string trimleft $fq :] ::] {}]
set keep [list]
foreach seg $parts {
if {[regexp {[*?]} $seg]} {
break
}
lappend keep $seg
}
return ::[join $keep ::]
}
proc emit_nsdict {nd opt_pathcommands opt_synopsis} {
set location [dict get $nd location]
out namespace $location
foreach {label key} {
children children
packagetails packagetails
packageprefixes packageprefixes
} {
set items [dict get $nd $key]
if {[llength $items]} {
out $label [lsort -dictionary $items]
}
}
set commands [dict get $nd commands]
out commands [llength $commands]
set classified [list]
foreach {label key} {
procs procs
aliases aliases
ensembles ensembles
native native
imported imported
ooclasses ooclasses
ooobjects ooobjects
ooprivateclasses ooprivateclasses
ooprivateobjects ooprivateobjects
coroutines coroutines
interps interps
zlibstreams zlibstreams
undetermined undetermined
} {
set items [dict get $nd $key]
if {[llength $items]} {
out $label [lsort -dictionary $items]
lappend classified {*}$items
}
}
set other [list]
foreach c $commands {
if {$c ni $classified} {
lappend other $c
}
}
if {[llength $other]} {
out other [lsort -dictionary $other]
}
set exported [dict get $nd exported]
if {[llength $exported]} {
out exported [lsort -dictionary $exported]
}
set documented [lsort -dictionary [dict get $nd usageinfo]]
if {[llength $documented]} {
out documented $documented
}
set exportpatterns [dict get $nd namespacexport]
if {[llength $exportpatterns]} {
out exportpatterns $exportpatterns
}
set pathdict [dict get $nd namespacepath]
if {[dict size $pathdict]} {
out namespacepath [dict keys $pathdict]
if {$opt_pathcommands} {
dict for {p pinfo} $pathdict {
set pcmds [list]
if {[dict exists $pinfo commands]} {
set pcmds [dict get $pinfo commands]
}
puts [format " pathcommands(%s): %s" $p [strip [lsort -dictionary $pcmds]]]
}
}
}
if {[dict get $nd itemcount] == 0} {
out note "(no members matched glob '[dict get $nd glob]' - namespace empty, or the package populating it is not loaded)"
}
if {$opt_synopsis && [llength $documented]} {
puts "synopsis:"
foreach c $documented {
set id [punk::ns::nsjoin $location $c]
if {![punk::args::id_exists $id]} {
# alias/import - resolve the documentation id the way cmdhelp does
set id ""
if {![catch {punk::ns::cmdinfo [punk::ns::nsjoin $location $c]} cinfo]} {
set docid [dict get $cinfo docid]
if {$docid ne "" && [punk::args::id_exists $docid]} {
set id $docid
}
}
}
set syn ""
if {$id ne "" && ![catch {punk::args::synopsis $id} s]} {
set syn [string trim [strip $s]]
}
if {$syn eq ""} {
puts [format " %s (no punk::args synopsis available)" $c]
} else {
foreach line [split $syn \n] {
puts " $line"
}
}
}
}
}
# ---------------------------------------------------------------- arg handling
set opt_synopsis 0
set opt_pathcommands 0
set globs [list]
foreach a $argv {
switch -- $a {
-synopsis {set opt_synopsis 1}
-pathcommands {set opt_pathcommands 1}
default {lappend globs $a}
}
}
if {![llength $globs]} {
puts stderr "usage: punk91 src script scriptlib/developer/nslist.tcl ?-synopsis? ?-pathcommands? <nsglob>..."
puts stderr " e.g: punk91 src script scriptlib/developer/nslist.tcl punk::ns"
exit 2
}
# ---------------------------------------------------------------- main
set failures 0
set output_started 0
foreach g $globs {
set fq [string map {:::: ::} $g]
if {![string match ::* $fq]} {
set fq ::[string trimleft $fq :]
}
set notes [list]
set base [nonglob_base $fq]
if {![namespace exists $base]} {
set loaded [try_autoload $base]
if {$loaded ne "" && [namespace exists $base]} {
lappend notes "loaded package '$loaded [package present $loaded]' to populate $base"
}
}
set query ""
if {![regexp {[*?]} $fq]} {
if {[namespace exists $fq]} {
set query ${fq}::*
if {[namespace ensemble exists $fq]} {
lappend notes "$fq is also an ensemble command - whatis.tcl shows its subcommand map"
}
} else {
# not a namespace - maybe a command (follow an ensemble to its namespace)
set resolved [uplevel #0 [list namespace which -command $fq]]
if {$resolved ne "" && [namespace ensemble exists $resolved]} {
set ensns [namespace ensemble configure $resolved -namespace]
lappend notes "'$g' is an ensemble command implemented in namespace $ensns - listing that (whatis.tcl shows the authoritative subcommand map)"
set query ${ensns}::*
} elseif {$resolved ne ""} {
puts stderr "nslist: '$g' is a command, not a namespace - use the tcl-whatis skill: punk91 src script scriptlib/developer/whatis.tcl [string trimleft $g :]"
incr failures
continue
} else {
puts stderr "nslist: no namespace (or command) '$g' found - and no package matching it could be loaded"
incr failures
continue
}
}
} else {
if {[namespace exists $base]} {
set query $fq
} else {
puts stderr "nslist: base namespace $base of glob '$g' not found - and no package matching it could be loaded"
incr failures
continue
}
}
if {[catch {punk::ns::get_ns_dicts $query -nspathcommands $opt_pathcommands} dicts]} {
puts stderr "nslist: punk::ns::get_ns_dicts failed for '$query': $dicts"
incr failures
continue
}
if {![llength $dicts]} {
puts stderr "nslist: no namespaces matched '$query'"
incr failures
continue
}
if {$output_started} {
puts ""
}
set output_started 1
foreach n $notes {
out note $n
}
set i 0
foreach nd $dicts {
if {$i > 0} {
puts ""
}
incr i
emit_nsdict $nd $opt_pathcommands $opt_synopsis
}
}
exit [expr {$failures == 0 ? 0 : 1}]

14
scriptlib/developer/whatis.tcl

@ -21,6 +21,9 @@
# 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.
# Companion: scriptlib/developer/nslist.tcl (tcl-nslist skill) answers the
# namespace-level question - what commands/child namespaces exist below a
# namespace or package at all.
proc strip {text} {
if {[info commands ::ansistrip] ne ""} {
@ -129,6 +132,17 @@ if {$resolved eq ""} {
}
}
if {$resolved eq ""} {
# not a command - if it names a namespace (possibly after loading the package of
# the same name), point at the namespace-discovery counterpart script
set nsname ::[string trimleft $name :]
if {![namespace exists $nsname]} {
catch {package require [string trimleft $name :]}
}
if {[namespace exists $nsname]} {
puts stderr "whatis: '$name' is a namespace, not a command - list its contents with:"
puts stderr " punk91 src script scriptlib/developer/nslist.tcl [string trimleft $name :] (tcl-nslist skill)"
exit 1
}
puts stderr "whatis: command '$name' not found (and no matching package could be loaded)"
exit 1
}

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

@ -1520,6 +1520,28 @@ tcl::namespace::eval punk::ns {
return $output
}
#cli command - impure - relies on caller/ns_current
#2026-07-19 Agent-Generated: documentation-only PUNKARGS (options are forwarded to
#get_ns_dicts unparsed - keep synchronized with that proc's PUNKARGS).
punk::args::define {
@id -id ::punk::ns::nslist_dict
@cmd -name punk::ns::nslist_dict\
-summary\
"Namespace-member info dicts for a possibly-relative namespace glob."\
-help\
"Convenience wrapper over get_ns_dicts accepting a relative glob: glob is
resolved against the calling namespace context (nspath_here_absolute).
Options and the returned list of dicts are as for get_ns_dicts."
@leaders
glob -type string -optional 1 -default * -help\
"Namespace glob, absolute or relative to the calling namespace."
@opts
-allbelow -type boolean -default 0 -help\
"Also report every namespace below those matched (see get_ns_dicts)."
-nspathcommands -type boolean -default 1 -help\
"Populate the per-entry 'commands' lists in the namespacepath dict
(see get_ns_dicts)."
@values -min 0 -max 0
}
proc nslist_dict {{glob "*"} args} {
set ns_absolute [uplevel 1 [list ::punk::ns::nspath_here_absolute $glob]]
return [get_ns_dicts $ns_absolute {*}$args]
@ -2990,7 +3012,67 @@ y" {return quirkykeyscript}
#glob chars at last segment match contents/children of namespaces
#glob chars in the path will result in multiple namespaces being matched
#e.g ::tcl::*::d* will match commands beginning with d and child namespaces beginning with d in any namespaces 1 below ::tcl
#2026-07-19 Agent-Generated: documentation-only PUNKARGS - the proc parses manually (dict
#merge); keep behaviour and definition synchronized. Motivated by exposing this data layer
#to agent tooling: scriptlib/developer/nslist.tcl (tcl-nslist agent skill) consumes it.
punk::args::define {
@id -id ::punk::ns::get_ns_dicts
@cmd -name punk::ns::get_ns_dicts\
-summary\
"Namespace-member info dicts for a fully-qualified namespace glob."\
-help\
"Return a list of dicts - one per namespace matched by fq_glob - describing
each namespace's members. This is the data layer behind the interactive
namespace listings (n/ n// n/// nslist) and suits programmatic namespace
discovery (e.g scriptlib/developer/nslist.tcl for agents).
Glob chars (* ?) in the last segment of fq_glob match members (commands and
child namespaces); glob chars in earlier segments match multiple namespaces.
A trailing segment of ** additionally reports every namespace below the
matched path.
e.g ::tcl::*::d* matches commands and child namespaces beginning with d in
any namespace one level below ::tcl.
Keys of each returned dict:
location - the fully-qualified namespace described
children - matching child namespace tails (dictionary-sorted)
packagetails - children that exactly match a loadable package name
packageprefixes - children that are a prefix of deeper package names
commands - all matching command tails (every kind)
procs - script procs ('info procs' in the namespace)
exported - commands matching the namespace export patterns
imported - commands imported from another namespace
aliases - interp aliases named in the namespace (an alias may
have no matching command if it was renamed away)
ensembles native coroutines interps zlibstreams ooobjects ooclasses
ooprivateobjects ooprivateclasses - commands by punk::ns::cmdtype
classification
namespacexport - the raw namespace export patterns
undetermined - reserved (currently always empty)
usageinfo - commands with punk::args (or tepam) documentation
namespacepath - dict keyed by each 'namespace path' entry of the
namespace; with -nspathcommands true each value is a dict whose
'commands' key lists the matching command tails resolvable via that
path entry (empty dicts otherwise)
glob - the member-matching glob applied (the last segment)
itemcount - count of matched members (children + commands)
Namespaces are reported even when itemcount is 0. The procs/exported/
imported/usageinfo lists are overlapping views over 'commands' (plus
commandless renamed aliases); 'commands' is the complete member list."
@leaders
fq_glob -type string -optional 0 -help\
"Fully-qualified namespace glob e.g ::punk::ns::*
(an error is raised if not fully qualified)."
@opts
-allbelow -type boolean -default 0 -help\
"Also report every namespace below those matched by the namespace-path
part of fq_glob (the member glob still applies within each)."
-nspathcommands -type boolean -default 1 -help\
"Populate the per-entry 'commands' lists in the namespacepath dict
(commands resolvable in the namespace via its 'namespace path'
entries). Use false to skip that work."
@values -min 0 -max 0
}
proc get_ns_dicts {fq_glob args} {
#manual args parsing (dict merge) - keep synchronized with PUNKARGS id ::punk::ns::get_ns_dicts
#puts stderr "get_ns_dicts $fq_glob"
set glob_is_absolute [expr {[string match ::* $fq_glob]}]
if {!$glob_is_absolute} {
@ -3008,9 +3090,6 @@ y" {return quirkykeyscript}
set nspathcommands [dict get $opts -nspathcommands]
# -- --- --- --- --- --- --- --- --- --- --- ---
set packagetails [list] ;#child namespaces which are an exact match for a package name
set packageprefixes [list] ;#child namespaces which are a prefix match for a package name - but not an exact match
#set location [nsprefix $fq_glob]
set commands [list]
@ -3437,6 +3516,14 @@ y" {return quirkykeyscript}
# set childtailmatches [lsort $childtailmatches]
#}
set childtailmatches [lsort -dictionary $childtailmatches]
#2026-07-19 Agent-Updated: packagetails/packageprefixes initialization moved here from
#before the report_namespaces loop: with multiple matched namespaces (path globs,
#trailing **) every dict after the first carried the union of all earlier namespaces'
#entries, mis-marking non-package children in nslist/n// underline styling and in
#get_ns_dicts data. Also: a child with several deeper packages below it was appended
#to packageprefixes once per matching package - now recorded once.
set packagetails [list] ;#child namespaces which are an exact match for a package name
set packageprefixes [list] ;#child namespaces which are a prefix match for a package name - but not an exact match
foreach ct $childtailmatches {
set fqchild [nsjoin $location $ct]
set searchname [string trimleft $fqchild :]
@ -3445,7 +3532,7 @@ y" {return quirkykeyscript}
#exact match.
lappend packagetails $ct
} else {
if {[string match ${searchname}::* $pkgname]} {
if {[string match ${searchname}::* $pkgname] && $ct ni $packageprefixes} {
#prefix match - but not exact match
lappend packageprefixes $ct
}

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

@ -1,6 +1,7 @@
0.9.0
0.9.1
#First line must be a semantic version number
#all other lines are ignored.
#0.9.1 - get_ns_dicts fix: packagetails/packageprefixes initialization moved inside the per-namespace loop - with multiple matched namespaces (path globs, trailing **) every dict after the first carried the union of all earlier namespaces' entries, mis-marking non-package children in nslist/n// underline styling and in the returned data; a child with several deeper packages was also appended to packageprefixes once per matching package (now once). Single-namespace queries (the nslist.test tier A pins) unaffected; the multi-namespace reset is pinned in nslist.test (nslist_classification_multins_package_buckets_reset). Documentation-only PUNKARGS added for get_ns_dicts (documents the returned per-namespace dict keys and -allbelow/-nspathcommands; parsing stays manual) and nslist_dict (caller-relative wrapper). Motivated by exposing the dict data layer to agent tooling: scriptlib/developer/nslist.tcl backing the new tcl-nslist agent skill.
#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.

22
src/tests/modules/punk/ns/testsuites/ns/nslist.test

@ -191,6 +191,28 @@ namespace eval ::testspace {
1 1 0
]
#added 2026-07-19 (agent) - regression pin for the punk::ns 0.9.1 fix: packagetails/
#packageprefixes are per-namespace - with multiple matched namespaces (path globs,
#trailing **) every dict after the first previously carried the union of all earlier
#namespaces' entries.
test nslist_classification_multins_package_buckets_reset {get_ns_dicts packagetails/packageprefixes reset per matched namespace (multi-namespace query)}\
-setup $fsetup -body {
set ds [punk::ns::get_ns_dicts ::navfix::**]
foreach d $ds {
if {[dict get $d location] eq "::navfix"} {
lappend result [list navfix tails [dict get $d packagetails] prefixes [dict get $d packageprefixes]]
} elseif {[dict get $d location] eq "::navfix::plainchild"} {
lappend result [list plainchild tails [dict get $d packagetails] prefixes [dict get $d packageprefixes]]
}
}
set result
}\
-cleanup $fcleanup\
-result [list\
{navfix tails {bothy tailpkg} prefixes {bothy prefixer}}\
{plainchild tails {} prefixes {}}\
]
test nslist_classification_native_builtin_ns {native bucket populates for a builtin namespace (fixture cannot contain native commands)}\
-setup {
set result ""

Loading…
Cancel
Save