d/ documented -choices on the v argument with -choiceprefix defaulting
to true, but the manual parser uses an exact string comparison. PUNKARGS
now declares -choiceprefix 0 so documented behaviour matches the
implementation (exact match required).
Assisted-by: harness=opencode; primary-model=openrouter/z-ai/glm-5.2; api-location=openrouter.ai
console_fact_get and console_fact_set documented -choices on the key
argument with -choiceprefix defaulting to true (prefix matching promised),
but the manual positional parser feeds key directly into a dict lookup
with no prefix normalization. PUNKARGS now declares -choiceprefix 0 so
documented behaviour matches the implementation (exact match required).
Assisted-by: harness=opencode; primary-model=openrouter/z-ai/glm-5.2; api-location=openrouter.ai
TIP 746 (Tcl 9.1) removed lseq's expr-operand behaviour. punk::lib::range
(lseq branch) now normalizes int[+-]int offsets via offset_expr so callers
like `range 0 [llength $list]-1` keep working (punk::ansi::grepstr broke
under 9.1b0, taking example-block highlighting and the punk::args
examples.test with it). The lseq branch is also aligned with the tcl8
fallback contract: default 'by' now infers direction (descending ranges
previously returned empty under tcl9) and 'by 0' returns empty (Tcl 9.1
lseq changed by-0 to return one element).
Direct lseq expression operands expr-wrapped: punk::lib lzipn_tcl9b/c,
cols, cols2; punk::args zero_based_posns.
check::has_tclbug_safeinterp_compile falls back to interp invokehidden
tcl:unsupported:disassemble - Tcl 9.1 safe interps hide tcl::unsupported::*.
New modules/punk/lib range.test pins the range contract on 9.0 and 9.1;
core tests AGENTS.md documents native-tclsh vs punk-exe exec.test baselines.
Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
Tests-first coverage pass (user-directed) before any changes to the
definition-parsing/indentation machinery. punk/args suite: 128 tests, 15 files.
- rendering.test (15): nesting independence (plain + tstr definitions render
identically regardless of source-block indentation), relative-indent
preservation (2-space help and 1-space choicelabel conventions, single and
multiline - preservation, not enforcement), -unindentedfields (arg -help
works left-margin; @cmd accepts-but-ignores it - GAP pinned), constructed
(string-built) definition absolute-indent characterization ('constructed'
to avoid confusion with the @dynamic directive), multiline ${[cmd]}/${$var}/
resolved_def insertions (aligned at insertion column, nesting-independent),
and @dynamic ${$DYN_X} double substitution: stable across repeated
resolutions, provider refresh honoured in help text, choice display and
parse acceptance, textblock::frame-style art choicelabels byte-aligned -
but multiline -help insertions land lines 2+ at insertion-4 (GAP pinned:
the second-round subst path lacks paramindent re-alignment)
- choicegroups.test (5): group/-choices union parsing, cross-group prefixes,
duplicate-entry dedup, per-group titled choice tables (the punk::imap4
multi-table display), imap4-style {\Deleted}/{$MDNSent} values with labels
incl. through tstr-bearing blocks; GAP pinned: prefix-normalization
list-quotes special-character choices (exact '\Deleted' stores the plain
string, prefix '\De' stores a one-element list) - value shape depends on
input style
- defquoting.test (3): container quoting rules pinned - braced values fully
literal ($, [], two-char \n, bare backslashes), quoted values get Tcl
backslash semantics (\n -> newline, \ -> \) with $/[] still literal, and
\$\{...\} renders a literal ${...} in tstr-processed blocks
- examples.test (6): helpers::example box/title/-tstr toggle (no-dedent
direct-call contract pinned), ${[example ...]} insertion into @cmd help
(bars at insertion column, content +2, automatic 'Example: eg <id>' row),
@examples -> punk::args::eg with #<nodisplay>/@#<nodisplay> stripping
(ANSI-tolerant), strip_nodisplay_lines unit, live tclcore ::lseq smoke
- punk::args 0.3.1: helpers::example -title path called bare [a] (resolvable
only where a punk shell's global alias exists - errored under plain tclsh);
now punk::ansi::a. Found by examples.test in the runtests environment.
- investigated punk::ansi::mark_columns first-call slowness (3.94s first,
60us after): argdoc -help calls a proc that calls mark_columns, whose parse
resolves the same still-resolving definition - hazard already documented
in-source with candidate fixes; recorded in CHANGELOG for goal
consideration, deliberately not patched
- project 0.4.11 (CHANGELOG entry)
Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
Activated and completed under user direction (2026-07-08). Aliases for choice
values are now first-class: accepted at parse, normalized to their canonical
choice in results, folded into the canonical entry in usage display, and
resolved identically by the punk::ns doc-lookup walk.
- punk::args 0.3.0:
* -choicealiases {alias canonical ...} on argument lines; dict shape
validated in the option switch, alias->existing-choice and no-collision
cross-validated after spec merge (surfaces at resolve time - define is
lazy)
* the choice-word matching chain extracted from get_dict into the shared
resolver punk::args::choiceword_match {word nocase allchoices
choicealiases choiceprefix denylist reservelist} -> {matched exact
canonical}; get_dict delegates to it (writeback/lset logic stays at the
call site) - one implementation for parser and doc walk
* exact aliases match under any -choiceprefix/-nocase setting; alias names
join the prefix-calculation pool when -choiceprefix is true; deny applies
to the matched NAME (denied alias requires the full alias; a canonical
reached via its alias is exempt from the canonical's own deny entry)
* display: aliases fold into the canonical entry as an '(alias: x)' /
'(aliases: x|y)' label note (single convergence point covers all render
paths); alias names join the display prefix calculation so highlighted
minimal prefixes stay truthful
* define doc gains the -choicealiases section
- punk::ns 0.1.2: cmd_traverse resolves subcommand words via choiceword_match
- -choiceprefixdenylist/-choiceprefixreservelist now honoured in doc lookup
(the two pinned parity GAP tests flipped to agreement), aliases normalize
before choiceinfo lookup, -nocase honoured in the walk
- punk 0.2.3: punk::help adopts the feature - 'i help' lists one entry per
registered topic (topics+help, tcl, env+environment, console+term|terminal);
topic words accept aliases and unique prefixes with the user-decided
minimum-prefix policy recorded in ::punk::helptopic: denylist {help}
(h/he/hel stay command words), reservelist {c to tc} (fall through to
command lookup; e/en and te/ter fall through naturally as ambiguous).
Argless 'help' output byte-identical to the strict-80 baseline.
- tests: choices.test +5 (alias exact/prefix normalization, noprefix exact-
only, nocase, the punk::help policy matrix, resolve-time validation);
cmdflow.test parity tests flipped from GAP to agreement + alias-traverse and
display-fold assertions (+flowaliased fixture)
- suites: punk/args 98 pass + 1 skip, punk/ns 28 pass, punk/lib 21 pass
- G-040 flipped active -> achieved 2026-07-08; detail file records
implementation and verification
- project 0.4.10 (CHANGELOG entry)
Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
Completes G-036's remaining acceptance item - version-based detection of the
vulnerable combination (tcludp < 1.0.13 on Tcl 9 Windows: the per-thread exit
handler closes the process-global sockListLock/waitForSock events, silently
freezing every other udp-using thread's event loop after any udp-loaded thread
exits; root-caused and fixed-by-upgrade in 0.4.3).
- punk::lib 0.3.0: has_libbug_udp_threadexit gathers live facts (loaded udp
version, else best available registered version discovered without loading
the binary via an unsatisfiable package require triggering the index scan)
and delegates the verdict to the pure classifier
libbug_udp_threadexit_applies (facts in, verdict out - testable). has_libbug_*
is the new check family for bundled/vendored library bugs; buginfo dicts may
carry a full 'url' reference key for non tcl-core trackers.
- punk 0.2.1: 'help tcl' scans has_libbug_* alongside has_tclbug_*, renders the
url key when present, and no longer errors on a triggered check carrying a
reference without a description (latent unset-indent fix).
- new checkbugs.test: classifier combination matrix, live-check dict shape and
classifier consistency, and a buginfo-contract test across all existing
check procs.
- verified: current kit (udp 1.0.13) reports bug=0 with no warning; a simulated
triggered has_libbug_* check renders description + url in the help tcl
warning block. Suites: punk/lib 21 pass, punk/ns 26 pass.
- G-036 flipped to achieved 2026-07-08 (index acceptance REMAINING annotated
DONE; detail file gains the Detection section). Open non-gating decisions
recorded in the detail file: punk8win (8.6) kit udp 1.0.12 swap; optional
upstream tickets for residual tcludp trunk weaknesses.
- project 0.4.7 (CHANGELOG entry)
Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
- commented out five development trace puts in the doc-lookup machinery:
"PROC auto def"/"ENSEMBLE auto def" (generate_autodef) were emitted on STDOUT
and polluted 'i'/'s' output in script/exec contexts (agent-facing per the
G-015 piped-call pattern); "cmd_traverse - skipping to documented subcommand"
(space-form id path), "---> cmd_traverse ensembleparam" (ensemble -parameters
traversal) and "cmd_traverse 10 ... - review" (fallthrough return) were
stderr noise. No functional change.
- cmdflow.test: 8 new tests characterizing ensemble -parameters handling in the
doc-lookup flow (the punk::netbox::man pattern - leading apicontextid before
the subcommand at every ensemble level):
* generate_autodef models each -parameters entry as a -ensembleparameter 1
leader with @leaders min/max = nparams+1 (single- and two-param fixtures)
* cmd_traverse consumes parameter words and resolves the subcommand's
explicit or autodef docid, re-presenting the parameter in args_remaining
so the subcommand's own leader-based definition (as punk::netbox::man's
explicit defs model the curried parameter) can parse it
* nested sub-ensembles with their own -parameters re-consume the curried
parameter at each level
* punk::args::parse treats -ensembleparameter leaders as ordinary named
leaders; cmdhelp renders a documented subcommand's usage through the
parameter (end-to-end smoke)
- suites: punk/ns 26 pass (was 18), punk/args 93 pass + 1 skip (unchanged)
- project 0.4.6 (CHANGELOG entry)
Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
- fixed "@opts -any 1" (arbitrary/adhoc option passthrough): an option not in
the definition crashed get_dict with "can't read argname" at the name-mapping
stage - adhoc opts now fall back to their raw supplied name, under which
argstate/arg_checks were already populated at scan time (real consumers:
vendored tomlish definitions declare -any 1)
- commented out a debug puts stderr (">>>_get_dict_can_assign_value NOT
alloc_ok...") that fired on every failed clause type assignment (visible on
any multiform parse miss; its happy-path twin was already commented)
- new forms.test (8 tests): @form spec structure (form_names, shared
{a b}-prologue blocks), explicit -form by name/index with cross-form
rejection, multiform synopsis rendering, and GAP pins for G-041 - default
parse is effectively form-0-only, -form rejects the documented list-of-forms
usage, and @form -synopsis overrides are stored in the spec but ignored by
synopsis rendering
- new validation.test (11 tests): -type validation matrix, -range/-minsize/
-maxsize, -nocase choice normalization, option -multiple accumulation,
required-option enforcement, unknown-option rejection vs -any adhoc
passthrough (the bugfix's regression test), -- end-of-options marker,
@values -max enforcement, received-key clause ordinals, solos list, and
default-vs-received distinction
- src/tests/AGENTS.md: note that tcltest compares the -body return value -
bodies ending in a loop must end with an explicit 'set result'
- suites: punk/args 93 pass + 1 skip (was 70+1), punk/ns 18 pass; full
source-tree run 569/581 with only the known exec-14.3 baseline failure
- project 0.4.5 (CHANGELOG entry)
Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
- GOALS.md: G-040 punk::args choice aliasing with parse normalization, display
folding, and doc-lookup parity (user-approved); detail file records the
prefix/deny/reserve collapse analysis from the punk::help work, the
cmd_traverse divergence, the c/to/tc minimum-prefix policy question, and the
test-first sequencing
- new punk/ns cmdflow.test (10 tests): cmdwhich/cmdinfo/cmd_traverse docid
flow - direct, via-alias and subhelp-choiceinfo resolution (exact + unique
prefix), unknown-word fallback to parent docid, -choiceprefix 0 exact-only,
space-delimited ids, cmdhelp -return string smoke; the parse-vs-doc-walk
divergence (deny/reserve lists honoured by punk::args::parse, ignored by the
cmd_traverse walk) pinned as GAP tests pending G-040
- choices.test: 4 new unrestricted-mode (-choicerestricted 0) tests - prefix
normalization to the canonical choice, unknown/ambiguous passthrough,
denylist and reservelist passthrough, and the punk::help collapse recipe
(prefix+deny+reserve on a -multiple leader)
- punk::args 0.2.2 (doc-only): define doc -choice* fields now state the
-choicerestricted 0 passthrough semantics, -choiceprefix result
normalization + minimal-prefix display highlighting, denylist behaviour for
shorter prefixes in both restricted modes, and the reservelist phantom-entry
idiom for per-choice minimum-prefix control
- suites: punk/args 74 pass + 1 skip (was 70+1), punk/ns 18 pass (was 8)
Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
app-punkscript accepts lib:<name> (with or without .tcl - extensionless
appends it) as the shell subcommand does: `punkexe script lib:hello` or
bare `punkexe lib:hello` via reclassification. The prefix always wins; a
literal path beginning lib: (pathological - colon is illegal in Windows
filenames) is reachable via ./lib:... Not-found errors list every
searched location; only .tcl runs via the script subcommand.
Resolution policy is factored into punk::path::scriptlib_resolve
(PUNKARGS-documented) rather than copied inline: kit-internal
app/scriptlib first and deliberately not externally overridable, then
scriptlib dirs relative to the executable - the same policy
app-punkshell encodes inline (todo noted: refactor the shell path onto
the shared proc). Share definitions via modules, not launcher control
flow.
Verified on both generations (punk902z, punksys): lib:hello
extensionless and explicit, bare-arg reclassification, not-found
candidate listing (bin/scriptlib then <root>/scriptlib for a bin/ exe).
Project version 0.4.0 (backward-compatible behaviour addition).
Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
app-punkscript loads punk::args::moduledoc::tclcore (catch-guarded,
mirroring the punk::repl code-interp setup) so core Tcl commands are
documented in script runs as at the repl: `''i list'' | punkexe script`
now renders the doc table instead of "Undocumented command". Cost ~40ms;
kit script startup ~0.33s. REVIEW note added: the default-punk-shell
package set is now expressed in app-punkscript AND the repl codethread
setup - consolidation into one shared definition is the G-015-noted
follow-on.
punk::packagepreference 0.1.1: moduledoc auto-load success notice moved
stdout -> stderr (stdout must stay machine-clean for script/exec
contexts; the failure branch already used stderr).
Verified on both generations (punk902z, punksys): i list doc table on
stdout, notice on stderr, timing unchanged. Build-order footnote: lib
changes need make.tcl libs before vfscommonupdate - the modules-only
first rebuild shipped a stale app-punkscript payload.
Project version 0.3.1.
Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
On a tcl 8.6 windows console in cooked (line) mode with a readable handler
armed on stdin - the idle-at-a-line-mode-prompt condition, e.g. a query
fired from an after-script or worker thread while the shell waits for
input - the 8.6 channel driver has a blocking cooked-mode ReadConsole
parked (arming posts it; driver reads sample the console mode at issue
time), so a terminal query's response is swallowed until Enter and then
leaks to the line reader as phantom input. This is the one remaining
hostage window after the 0.7.1 detection fixes and repl 0.2.2 read
discipline: it is the reader legitimately doing its job, so no repl-side
change can remove it (and line mode remains supported for user
scripts/mini-apps even once raw becomes the default).
get_ansi_response_payload now detects the condition before emitting
(cooked + no -inputmode key + twapi console handle + armed readable
handler) and refuses fast with errorcode
{PUNK CONSOLE QUERY HOSTAGE_COOKED_READ} - no emission, no ~500ms timeout,
no input corruption. Mid-command queries (repl reader disarmed) and raw
mode are unaffected. Best-effort by design: a parked read can outlive a
removed handler, so the guard catches the systematic case only. The G-007
routing wrapper now preserves the owner-side errorcode so brokered callers
can discriminate the refusal.
Tests: constraint-gated guard test in probes.test (engages on a real 8.6
console, self-skips on 8.7/9 via the -inputmode key and in piped runs);
ownerrouting.test passes with the errorcode-preserving wrapper. Full suite
at baseline (exec-14.3 only) under Tcl 9.0.3; probes suite also verified
under Tcl 8.6. Project version bumped to 0.2.2 with CHANGELOG entry per
the versioning policy (user-visible failure-mode change).
Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
get_size on tcl 8.6 returned 'columns 1' (real size e.g. 260x49): with the
0.7.1 console-detection fixes in place, 8.6 takes the ANSI size mechanism
(no -winsize shortcut) for the first time under G-007 routing - and that
mechanism is compound: the far-corner cursor move was written unflushed
into the calling thread's stdout channel instance while the routed position
query executed and flushed in the console-owning thread's separate instance
of the same OS handle. The terminal answered before the move reached it.
Pre-routing this was masked because emit and query shared one channel
instance, so the query's own flush pushed the emissions too.
All compound emit-then-query operations now flush their emissions before
querying: get_size_using_cursormove and get_size_using_cursorrestore (the
far-corner move), test_char_width (both the positioning emission and the
measured test emission - the latter would silently corrupt character-width
results), and test_string_cursor (alt-screen/move/erase). Error paths flush
their cursor-restore emissions likewise.
Interactively verified on punksys (tcl 8.6.13, src launch): get_size
reports the true terminal dimensions and test_char_width \t returns 8.
Full suite passes at baseline (exec-14.3 only) under Tcl 9.0.3.
Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
Two wiring-order holes surfaced during the G-007 interactive verification:
1. Calling ensure_object_integration before opunk::console is loaded errored
into the nonexistent namespace (can't set "::opunk::console::
waiting_chunks_arrayvar": parent namespace doesn't exist). It now returns
0 as a graceful no-op without latching object_integration_done, so a
later call after opunk::console loads still performs the wiring; the
wired/latched path returns 1.
2. 'package require opunk::console; opunk::console::create ...' with no
intervening punk::console object operation left the created console
without a registered owner: the lifecycle callback that records
ownership is only installed by ensure_object_integration, which is wired
lazily from punk::console's object-spec paths. Behaviour was unaffected
for the owning thread (share qualifiers fall back to the calling thread)
but other threads could not address that console's facts. On wiring the
callback, ensure_object_integration now retro-registers ownership for
anchors already present in the interp - anchors are per-interp/per-thread
so the anchoring context is the calling thread. Only empty registry
entries are filled; existing live registrations are preserved (and the
default console keeps first-registration-wins semantics).
Tests: new objectintegration.test covers the no-op return (unlatched),
retro-registration of a pre-wiring anchor with lifecycle forget clearing
the entry, idempotency of the latched path, and normal post-wiring
registration - passing under Tcl 9.0.3 and 8.6. Full suite at baseline
(exec-14.3 only).
Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
Companion to punk::console 0.7.1 (three-site 8.6 console misdetection fix).
The line-mode waiting-chunks path in repl_handler performed an unsized read
that, entered via 'after idle' with the channel drained, made the tcl 8.6
windows console driver park a blocking cooked-mode ReadConsole (driver reads
sample the console mode at issue time - a later raw flip cannot cancel one).
After typed-ahead was stashed during a terminal-query raw window, that
parked read swallowed every subsequent query response in the session until
Enter, with the responses later leaking to the reader.
On hostage-prone consoles (windows, no -inputmode configure key, real twapi
console handle) the path now consumes only data already in the Tcl channel
buffer (chan pending input + sized read - no driver probe). With nothing
buffered it processes the stashed complete lines directly - preserving the
recovered-typeahead execution order - and arms the readable handler for any
remaining partial line, replacing an after-idle reinvoke that, without the
read, could never have progressed; the parked read that arming creates is
the reader legitimately waiting for the user to finish typing. Behaviour on
tcl 9/8.7 (-inputmode consoles) and in raw mode is unchanged.
Interactively verified on punksys (tcl 8.6.13, src launch): the previously
failing case - typing a full command during a line-mode brokered query loop
- now runs with all queries answering, the typed line executing after the
loop, and no timeouts, response leakage or chan-blocked diagnostics; the
session remains healthy afterwards. Full suite passes at baseline
(exec-14.3 only) under Tcl 9.0.3.
Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
Tcl 8.6 windows console channels expose no -inputmode configure key, so
punk::console guards testing -inputmode/-mode classified a real 8.6 console
as a pipe. A twapi console handle for stdin is now treated as definitive
console detection at all three affected sites (stdin only - pipe probes
still never touch the process console; mintty-as-pipes has no console
handle and falls through to the env heuristics as before):
1. get_ansi_response_payload's raw-cycling gate (introduced 0.1.5) skipped
the raw cycle for 8.6 line-mode queries.
2. is_input_console_or_tty returned a false negative, making
settle_can_respond's layer-2 heuristic settle a real 8.6 console as
unable to respond.
3. input_at_eof (introduced 0.1.2) took the pipe branch and performed its
probe read on the drained console channel. This was the line-mode query
killer: verified empirically against clean tclkits 8.6.13 and 8.6.17,
a read on a drained 8.6 console channel makes the channel driver park a
blocking cooked-mode ReadConsole; driver reads sample the console mode
when issued, not when data arrives, so the query's subsequent raw flip
cannot rescue it and the terminal response is swallowed until Enter.
Since get_ansi_response_payload calls input_at_eof immediately before
its raw cycle, every 8.6 line-mode query timed out (~500ms) with the
response later leaking to the line reader as phantom input - also the
source of increased ANSI artifacts (e.g. 'help env') once 0.7.0
brokering made code-interp queries actually reach the terminal.
Interactively verified on punksys (tcl 8.6.13, src launch): line-mode
get_cursor_pos and dec_get_mode succeed mid-command, brokered query loops
run clean, and typed-ahead lines are recovered without phantom input.
Known residue (documented in the G-007 detail notes): queries fired while
the repl reader is armed and idle at a line-mode prompt (including some
repl-init detection queries) still hit the parked-read limitation on 8.6 -
repl init ordering is a follow-up; raw mode is unaffected.
Tests: probes.test gains constraint-gated wiring tests for the twapi
console branch of is_input_console_or_tty and input_at_eof (skip in piped
runs, engage on a real console). Full suite passes at baseline (exec-14.3
only) under Tcl 9.0.3; probes suite also verified under Tcl 8.6.
Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
A terminal query (internal::get_ansi_response_payload and every query proc
layered above it - get_cursor_pos, dec_get_mode, ...) on the process-default
console {stdin stdout} issued by a thread that is not the registered owner is
now forwarded whole to the owning thread via synchronous thread::send, so the
query queueing, raw-mode cycling, cooperative reader handling
(input_chunks_waiting) and settled can_respond gating all execute in the
owner's context. Owner-side errors propagate to the caller with routing
context. Routing happens before the local can_respond gate deliberately: a
non-owner context's anchored view of the default console may be unsettled
while the owner's is settled.
New internal::console_route_owner makes the decision: routing applies to the
default console pair only - non-std channel names are thread-local, so an
{in out} pair spec always names the calling thread's own console and operates
locally (a console constructed and owned by code-interp/worker code
round-trips nowhere). Unregistered, owner==caller and dead-owner
(liveness-validated) cases also operate locally, preserving single-interp
behaviour exactly.
The synchronous send relies on the owner servicing events while the caller
blocks - the property the repl-installed vt52/colour/mode aliases already
depend on. Those aliases are unaffected: a call arriving in the owner
resolves to owner==self and takes the local path, so no double-hop and no
ping-pong.
Tests: new ownerrouting.test covers the routing decision (unregistered/self/
other-live-thread/non-default-pair/dead-owner) and the transport (recorder in
a worker-thread owner: args marshalled intact, execution in the owner thread,
query procs above the choke point inherit the routing, owner-side error
propagation). Full suite passes (baseline exec-14.3 only).
G-007 remains active: the remaining acceptance verification is interactive -
a terminal query from a live punk session's code interp against the default
console (cooperating with the repl reader), which needs a real responding
terminal.
Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
Per-console facts are now visible to every thread of a punk session: the
default console {stdin stdout} keeps its legacy namespace variables as
authoritative local storage, with write traces mirroring every write
(including direct variable writes) into tsv punk_console_facts; non-default
consoles store facts only in tsv with owner-qualified keys (non-std channel
names are thread-local and would otherwise alias). console_fact_set from a
non-owner thread forwards to the owning thread (vt52-alias transport) and
always writes the tsv mirror itself (covers unreachable or older-version
owners). New console_fact_clear for tests/maintenance.
New console ownership registry (console_owner_register/get/forget, tsv
punk_console_owners keyed by canonical {in out} pair): ownership is captured
when an opunk::console instance is anchored - via the new pluggable
::opunk::console::lifecycle_callback (opunk::console 0.4.0, base class
unchanged) wired by ensure_object_integration - and by default_console.
Consult-time liveness validation clears entries for exited threads. For
{stdin stdout} first registration wins and only the owner's forget releases
the entry, so a thread anchoring a local view cannot steal ownership.
dec_has_mode/ansi_has_mode caches moved to tsv punk_console_modecache
(single-key atomic entries shared process-wide).
Infrastructure tsv arrays are now punk_-prefixed to avoid collisions with
application tsv usage in subshells: console -> punk_console (is_raw), with
call-site patches in punk::repl 0.2.1, punk::lib 0.2.1, punk::basictelnet
0.1.1 (rule recorded in src/modules/AGENTS.md; legacy repl/codethread_*/
zzzload_pkg* arrays left for a coordinated follow-up).
punk::console is now loadable in secondary threads without ::argv0
(powershell consolemode fallback guarded).
Tests: consolefacts gains ownership-registry and cross-thread visibility
coverage (worker-thread punk::console load, trace-mirror, forwarded set with
deadline-polled sync - see comments re runtests child-interp topology);
cleanups use console_fact_clear / tsv instead of poking removed internal
stores. Full suite passes (baseline exec-14.3 only).
G-007 flipped proposed -> active (user-confirmed); remaining acceptance work
is the choke-point brokering slice.
Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
Migration plan phase 4 (final phase):
- get_cursor_pos, get_cursor_pos_list, get_checksum_rect,
get_device_status, get_device_attributes,
get_device_attributes_tertiary, get_terminal_id, get_tabstops,
get_tabstop_apparent_width, get_dimensions, get_xterm_size,
get_xterm_pixels, dec_get_mode_line_wrap and ansi_get_mode_LNM
accept -console <consolespec> (any spec form) or the legacy
trailing positional, parsed by new internal::hybrid_console_spec;
each carries a PUNKARGS definition (@leaders per the Argument
Order rule)
- get_size refactored onto internal::hybrid_console_spec
(behaviour unchanged)
- documentation-only PUNKARGS for input_at_eof,
is_input_console_or_tty, size_via_query_mechanisms,
console_size_provider and the get_size_using_* mechanism helpers
(signatures unchanged - internal size mechanisms remain
canonical-pair only, per the updated AGENTS.md contract)
AGENTS.md migration bullet rewritten from 'incrementally migrating'
to the completed-state contract. New queryprocs.test (6 tests,
fed-response coverage for every converted proc across spec forms);
full console suite 58/58 on Tcl 9.0.3.
Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
Migration plan phase 1:
- dec_set_mode/dec_unset_mode/ansi_set_mode/ansi_unset_mode and
cursor_style resolve -console via console_spec_resolve instead of
'[lindex $terminal 1]' - anchored opunk::console instance names and
::opunk::Console object values now work (previously silently wrote
to the wrong target)
- the fourteen '-console -type list -minsize 2' declarations replaced
with new shared fragment ::punk::console::argdoc::console_opts (the
-minsize 2 constraint rejected 1-element instance-name specs at
parse time); show_input_response and cell_size -console docs unified
on the fragment
- dec_has_mode/ansi_has_mode cache on the canonical {in out} pair -
all spec forms addressing the same console share one cache entry
- test_is_vt52 option -inoutchannels renamed to -console per the
documented convention (experimental/broken proc, no callers)
- set/unset mode argdocs corrected: <codes> are semicolon delimited
AGENTS.md records the fragment usage and the -minsize 2 prohibition.
3 new tests in consolespec.test (setter spec forms, cache
canonicalisation via mixed specs, test_is_vt52 -console); full console
suite 52/52 on Tcl 9.0.3.
Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
The codethread init_script now propagates package prefer via a new %packageprefer% scriptmap entry. The code interp (punk/0 repltype) creation replaces the manual epoch-only copy with punk::lib::interp_sync_package_paths code -libunknown 1, which propagates tm list, auto_path, package prefer, and libunknown epoch+init from the codethread to the code interp. This fixes the issue where package prefer latest set in punk_main.tcl was not inherited by the REPL's code interp, causing 999999.0a1.0 dev modules to be passed over in favour of stable VFS-bundled versions.
snapshot_package_paths returns a script string reproducing the caller's tcl:™️:list, auto_path, and package prefer for use in thread::create init scripts. With -libunknown 1 it also copies the punk::libunknown epoch and sources+inits libunknown in the target. interp_sync_package_paths now propagates package prefer (was missing) and accepts optional -libunknown 1 flag for epoch copy + libunknown init in the child interp. Both procs have PUNKARGS argdoc documentation. Includes 6 tcltest tests covering basic propagation, version selection with package prefer latest, ordering preservation, snapshot script validity, -libunknown flag, and regression.
tm_path_additional_ifneeded referenced an out-of-scope \ variable that was never a parameter or local, causing 'can't read project_root: no such variable' whenever a #modpod module was found. The proc now requires project_root as a 2nd parameter. Additionally, the #modpod directory pattern was hardcoded to 999999.0a1.0 which the build system replaces with the real version (e.g 0.3.0) during make.tcl modules — breaking pattern matching in built bootsupport copies that looked for #modpod-*-0.3.0 directories which don't exist on disk. The proc now extracts both modname and version dynamically from the #modpod-<modname>-<version> directory name by splitting on the last dash, making it version-agnostic. Punk::lib::tm_version_magic convention was considered but dynamic extraction is preferred here as it will also work in the future src package_mode boot context where punk modules aren't loaded yet.
parse_testrun now keeps the full Tcl errorInfo in testcase_fails entries for ERROR-status failures (previously parsed then discarded), and captures the actual vs expected result blocks for FAILED-status result mismatches via a new result_stage sub-state. Test name extraction in the took/PASSED/SKIPPED handlers is fixed to use the full substring between the structural delimiters instead of lindex \ 1, which corrupted timing associations and testcase_passes/testcase_constraintskips keys for any test name containing spaces; the took handler uses string last to allow test names that themselves contain the word 'took'. The closing-FAILED microseconds lookup now keys on \ (authoritative) instead of the stale \. New dict keys are additive; existing consumers using dict exists/dict get are unaffected.
- disableAnsi was defined as a duplicate enableAnsi in the windows
no-twapi branch, leaving disableAnsi undefined there
- get_size_using_cursormove flushed stdout instead of the supplied
output channel
- get_size_using_chanconfigure: removed unreachable code after its
unconditional error
- cell_size: query path now passes its -console spec to get_size
instead of always querying the default console; bad-format error
message named punk::sixel instead of punk::console
- test_string_cursor: undefined $test_string reference in the no-ansi
path; rowoffset/columnoffset return labels were swapped
- ansi::move_emit vt52 branch called nonexistent punk::ansi::v52move_emit
(now vt52move_emit)
- move_emit_return re-expanded {*}$args inside its per-triple loop,
duplicating emissions when extra row/col/data triples were supplied
- dec_has_mode unknown-mode error message named dec_get_mode
Console module test suites pass (27/27, Tcl 9.0.3).
Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
Resolves the build-maintenance TODO: built module trees no longer accumulate
superseded intermediate versions, and .punkcheck state stays consistent because
every deletion is recorded through punkcheck's event mechanism.
punkcheck (0.3.0):
- installsource_add_virtual + installevent method targetset_addsource_virtual:
'virtual' named-value SOURCE records (-type virtual -path virtual:<id> -value <v>)
compared by stored value, participating in targetset_source_changes. Needed
because successive module versions are built from the same physical source
files - the recorded virtual module_name/module_version identify which
product of the source fileset a target is, at install and delete time.
punk::mix::cli (0.5.0):
- lib::prune_superseded_target_modules: deletes lower-version .tm siblings only
when records prove the same module line (virtual module_name match, or legacy
fallback: recorded sources share the keep-version's source folder(s) and name
prefix). Unrecorded files are never deleted - reported to stderr instead.
- lib::prune_sourcevanished_targets: mirror-prune of recorded targets whose
recorded source files no longer exist (layout bootsupport copies, vendormodule
copies) - intentional multi-version vendoring is preserved.
- build_modules_from_source_to_base prunes after each module install/skip and
records virtual module identity sources on installs.
make.tcl:
- bootsupport: non-glob include_modules.config entries track only the latest
version - superseded recorded snapshot versions pruned; project-layout copies
mirror-pruned; virtual identity recorded on snapshot installs. Calls guarded
(info commands) so a stale bootsupport snapshot degrades to a warning and the
two-pass modules+bootsupport bootstrap self-heals.
- vendormodule copies in root modules*/ mirror-pruned after install.
docs/config:
- include_modules.config headers now document the entry format (non-glob =
latest-only + prune; glob chars = keep all matches; unrecorded never pruned)
- src/AGENTS.md TODO replaced with the durable prune contract;
src/bootsupport/AGENTS.md + src/tests/modules/AGENTS.md updated
tests: src/tests/modules/punk/mix/testsuites/cli/prune.test (19 tests) covering
virtual source recording/comparison and prune identity/safety rules.
Suites: punkcheck 64/64, full suite under Tcl 9.0.3 green except pre-existing
exec-14.3.
payload sync: bootsupport snapshots, project layouts and _vfscommon.vfs pruned
of superseded intermediates (~570k lines of stale .tm copies removed).
Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
opunk::console (0.3.0):
- base size method resolution order: -winsize fast path -> capability gate (settled or heuristic
can_respond; unresponsive consoles get default_size with no query emission) -> pluggable
::opunk::console::size_query_provider -> default_size. The provider hook follows the
waiting-store pattern: the class carries no punk::console dependency, and non-channel
subclasses (tk widget / channel-environment terminals) simply override size.
punk::console (0.1.7):
- get_size dispatches object-governed calls through the virtual ::opunk::Console::size method
after first-use settling - subclass size overrides are honoured from every punk::console
call site
- ANSI/tput mechanisms + per-pair timing cache factored into size_via_query_mechanisms, shared
by the legacy channel-pair path and the registered provider (console_size_provider)
- integration wiring consolidated into once-latched ensure_object_integration (probe-byte store
redirect + provider registration, respecting customised providers), invoked from
default_console, console_spec_resolve lazy-require sites and the get_size object path
- legacy path returns the documented 80x24 fallback instead of erroring when undetermined
tests: WidgetishConsole subclass override returned through punk::console::get_size for both
anchored-name and object-value specs; class-level provider consultation/capability-gating with
a counting fake provider; provider registration assert. Console suites 27/27 + 10/10; punkexe
6/6, goals 8/8; full suite green except pre-existing exec-14.3.
Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
opunk::console (0.2.0):
- probe-byte waiting store changed from dict to array (::opunk::console::waiting_chunks) and made
pluggable via waiting_chunks_arrayvar: integrating layers redirect it to their established
cooperative store so bytes consumed by opunk::Console probe reads stay visible to active readers.
The class remains free of punk::console dependencies - redirection is the integrator's act.
punk::console (0.1.6):
- default_console redirects the opunk::console store to punk::console::input_chunks_waiting
(closing the split-store hazard that previously made opunk-level probes invisible to the
punk repl reader)
- get_ansi_response_payload performs first-use active settling for persistable console specs,
guarded by a recursion latch around settle_can_respond's own probe: every query path
(get_cursor_pos, get_device_status, ...) now pays at most one settling probe instead of a
fresh timeout per call on unsettled ambiguous consoles
punk::repl (0.1.6):
- repl::init routes default console construction through punk::console::default_console
(deduplicates creation logic and gains the store unification)
tests: store-unification and get_cursor_pos first-use settling cases in consolespec.test
(zero-emission and persistence asserts); consoleclass.test store assertions converted to array
form. Console suites 25/25 + 9/9; punkexe 6/6, goals 8/8; full suite green except pre-existing
exec-14.3.
Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
punk::console (0.1.5):
- settle_can_respond: layered active settling of a console's response capability, persisted on
the anchored opunk::console instance. Layer 1 certainty (input eof -> 0, no emission),
layer 2 strong-negative heuristic (plain pipe, no terminal hints -> 0, no emission - piped/CI
output streams stay clean), layer 3 active CSI 6n probe with timeout, only in the ambiguous
zone (real consoles / pipe-presenting terminals such as mintty without winpty). -force
unsettles and re-probes; -timeout_ms overrides can_respond_probe_timeout_ms (default 500).
Unmatched input consumed while waiting is preserved via input_chunks_waiting.
- get_size triggers first-use settling for persistable specs only (anchored instance name or
the auto-attached default) so unanchored object values cannot cause per-call probes
- get_ansi_response_payload only cycles console raw mode when the input channel is a
console/tty: pipe-targeted probes no longer flip the real console's mode as a side effect,
and a repeat-cycle deadlock under captured-channel environments (runtests runx) is fixed.
(Latent enableRaw_twapi/disableRaw_twapi restore asymmetry noted for future attention.)
punk::repl (0.1.5) / opunk::console (0.1.1): docs reference the active settling mechanism
(no behaviour change; probe machinery stays in punk::console so the class remains free of
that dependency)
tests: 5 new settle_can_respond cases in consolespec.test - certainty and clean-pipe settling
with zero-emission asserts, ambiguous probe timeout with emission and input-preservation
asserts, response arrival settling 1 with realistic event-loop delivery then -force
transition to 0, and get_size first-use settling. Console suites 23/23; full suite green
except pre-existing exec-14.3.
Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com