Compare commits

...

3 Commits

Author SHA1 Message Date
Julian Noble 456fa80500 goals: G-021 fold in capture-privacy model (Tier 1) 3 weeks ago
Julian Noble 99af43c8be goals: G-062 achieved archival + archive-loss prevention (sweep rules, backlink markers, goals_lint) 3 weeks ago
Julian Noble c309ed4782 punk::args 0.6.0: small restricted choice sets display as literal alternates in synopses 3 weeks ago
  1. 12
      AGENTS.md
  2. 8
      CHANGELOG.md
  3. 4
      GOALS-archive.md
  4. 6
      GOALS.md
  5. 24
      LICENSE.txt
  6. 2
      README.md
  7. 7
      goals/AGENTS.md
  8. 2
      goals/G-002-non-nested-subshell.md
  9. 5
      goals/G-005-zig-build-infrastructure.md
  10. 2
      goals/G-008-scoped-console-state.md
  11. 1
      goals/G-010-subshell-tree-navigation.md
  12. 1
      goals/G-011-console-stderr-semantics.md
  13. 3
      goals/G-012-template-payload-safety.md
  14. 4
      goals/G-013-raw-mode-default.md
  15. 4
      goals/G-014-punk-config-toml.md
  16. 3
      goals/G-016-projects-work-git-discovery.md
  17. 4
      goals/G-017-agent-project-discovery.md
  18. 1
      goals/G-020-screencap-input-module.md
  19. 72
      goals/G-021-agent-visual-verification.md
  20. 5
      goals/G-022-fossil-rename-punkshell.md
  21. 5
      goals/G-024-mapvfs-toml.md
  22. 1
      goals/G-025-exe-selfreport.md
  23. 3
      goals/G-027-derived-project-pull-updates.md
  24. 1
      goals/G-030-maketcl-punkargs.md
  25. 9
      goals/G-031-componentized-kit-boot.md
  26. 1
      goals/G-032-launcher-punkargs.md
  27. 1
      goals/G-033-proj-mode-cwd-project.md
  28. 1
      goals/G-038-piped-session-continuity.md
  29. 1
      goals/G-039-orphan-console-spin.md
  30. 1
      goals/G-041-punkargs-form-matching.md
  31. 5
      goals/G-042-subshell-help-topics.md
  32. 3
      goals/G-044-repl-command-completion.md
  33. 1
      goals/G-045-punkargs-authoring-ergonomics.md
  34. 2
      goals/G-050-synopsis-validity-marking.md
  35. 1
      goals/G-052-oo-method-autodef.md
  36. 1
      goals/G-053-punkargs-multiple-ranges.md
  37. 1
      goals/G-055-tclcore-regen-workflow.md
  38. 1
      goals/G-056-punkargs-word-wrapping.md
  39. 1
      goals/G-060-qemu-test-matrix.md
  40. 1
      goals/G-061-pseudoconsole-expect.md
  41. 1
      goals/G-063-package-license-tracking.md
  42. 11
      goals/archive/G-015-script-subcommand-piped-stdin.md
  43. 19
      goals/archive/G-062-project-license-file.md
  44. 3
      punkproject.toml
  45. 218
      scriptlib/developer/goals_lint.tcl
  46. 66
      src/modules/punk/args-999999.0a1.0.tm
  47. 3
      src/modules/punk/args-buildversion.txt
  48. 2
      src/tests/modules/AGENTS.md
  49. 9
      src/tests/modules/punk/args/testsuites/args/rendering.test
  50. 182
      src/tests/modules/punk/args/testsuites/args/synopsis.test

12
AGENTS.md

@ -38,6 +38,14 @@ Update the closest owning AGENTS.md when a change affects:
Update parent docs when parent-level structure, ownership, workflow, or child index changes. Update child docs when parent changes alter local rules. Remove stale or contradictory text immediately. Small edits that do not change behavior or contracts may leave docs unchanged, but the DOX pass still must happen.
## Doc Restructures
Rules for any bulk restructure, normalisation, or migration of documentation (index reshapes, field-format normalisation, file moves/renames across many docs):
- Conservation check before committing: extract the deleted lines from the diff and account for every non-blank one - it must be relocated verbatim somewhere in the new tree, be pure boilerplate/stub text (e.g. "see index (canonical)" placeholders), or be a deliberate drop named explicitly in the commit message. A preservation claim in a commit message ("bodies preserved untouched") must be backed by this check, not intent. Mechanics: `git diff | grep '^-[^-]'` (or the VCS equivalent), then `grep -F` each deleted line against the restructured files.
- Structured-field normalisation is the highest-risk step: prose riding a structured line (a status field, a header mirror) is invisible to a "preserve bodies" intent and is exactly what normalisation deletes. Relocate such prose to a body section first, then normalise.
- Separate moves from edits: a file move/rename commit is content-identical; any reconciliation edits land in an adjacent commit. Combined move+edit diffs are where losses hide.
## Hierarchy
- Root AGENTS.md is the DOX rail: project-wide instructions, global preferences, durable workflow rules, and the top-level Child DOX Index
@ -185,7 +193,7 @@ The project version is fully independent of module versions. A module bump (even
- `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 (created at user request 2026-07-11, e.g `tkconsole_demo.tcl` for the G-001 tk console backend); agents may add or update demos 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); 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`
@ -195,7 +203,7 @@ The project version is fully independent of module versions. A module bump (even
## Repo-wide Notes
- Punkshell (a.k.a. Punk Shell) is an experimental, alpha-level alternative Tcl shell under the BSD license; its primary language is Tcl. Note: "shellspy" (lowercase) refers only to the experimental `shellspy` subcommand available at launch — it is not the project name.
- Punkshell (a.k.a. Punk Shell) is an experimental, alpha-level alternative Tcl shell licensed under BSD-2-Clause (see `LICENSE.txt` at the repo root); its primary language is Tcl. Note: "shellspy" (lowercase) refers only to the experimental `shellspy` subcommand available at launch — it is not the project name.
- 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`.

8
CHANGELOG.md

@ -5,6 +5,14 @@ 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.12.1] - 2026-07-11
- G-062 achieved: canonical project license declared as BSD-2-Clause. `LICENSE.txt` added at the repo root with the standard BSD-2-Clause text (copyright Julian Marcel Noble, 2023-2026); `README.md` names BSD-2-Clause and points at `LICENSE.txt`; root `AGENTS.md` Repo-wide Notes names the license precisely; `punkproject.toml` `[project]` carries `license = "BSD-2-Clause"`. Repo-sweep found no top-level license claims contradicting it (vendored library licenses and module-level generated docs are not project-level claims).
## [0.12.0] - 2026-07-11
- punk::args 0.6.0: command synopses (`s <cmd>`, `i <cmd>`, usage headers) now display small restricted choice sets as literal alternates — an argument whose choice pool (`-choices` plus `-choicegroups` members) has 1–3 members and `-choicerestricted` true renders those words unitalicised joined by `|` (e.g. `after cancel` shows literal `cancel`; a 3-choice option shows `(left|centre|right)`), matching the existing display style of `literal()`/`literalprefix()` type-alternates. Larger or unrestricted choice sets keep the italicised argname/type display and an explicit `-typesynopsis` always wins, so e.g. `textblock::frame -type (choice|<dict>)` is unchanged. Synopsis rendering of type-alternates, `-typesynopsis` passthrough (including documenter ANSI styling), and multi-element clause display gained characterization test coverage (`synopsis.test`).
## [0.11.0] - 2026-07-11
- G-001 achieved: an interactive REPL can be launched against a non-detectable terminal-like device via an `::opunk::Console` subclass — `repl::init -console <spec>` selects the console (channel pair, anchored instance name, or object value), `repl::start`'s input channel defaults to it, and the repl's output (prompts, results, and the code interp's stdout/stderr — diverted via shellfilter junction stacks and emitted per run) routes through the selected console's channels, with eof/size/capability answered by the subclass overrides (punk::repl 0.4.0; base `::opunk::Console` and `punk::console` untouched). Verified end-to-end by child-process driver tests: an `::opunk::SshConsole` socket session whose scripted remote terminal answers `CSI 6n` (size resolved over the socket) and a `::opunk::TkConsole` text widget wired as a live console by new `opunk::console::tk::console` (reflected output channel rendering into the widget + Return-binding input pipe, opunk::console::tk 0.2.0). Process-console behaviours (tcl_interactive prompt gating, stdin reopen on eof, raw-mode re-enable, the windows line re-decode experiment) now apply only when no foreign console is selected; default stdin/stdout repl behaviour is unchanged.

4
GOALS-archive.md

@ -61,3 +61,7 @@ Acceptance: a punk91-style kit (tclsfe-x64 + punk9win.vfs, no thread dll in the
### G-059 [achieved 2026-07-11] WSL detection and suitability probing for driving unix-side tests from Windows → detail: goals/archive/G-059-wsl-test-driving.md
Scope: src/tests/ (capability probe helpers + constraint-gated cases in existing suites, e.g the unix sh-payload execution test in modules/punk/mix/testsuites/scriptwrap/multishell.test and a runtime.bash behaviour test), src/tests/AGENTS.md (enablement notes)
Acceptance: a documented probe helper yields a wsl_linux_available constraint whose checks are capability-based (distro launches and answers uname/tool probes; staging into a native tempdir works) and which cannot misfire on wsl.exe-present-but-unusable installs (no distro, WSL1 limitations, broken interop); the currently unix-gated multishell sh-payload execution test runs green via WSL on a suitable machine and still skips cleanly elsewhere; at least one runtime.bash behaviour test (active/use/run resolution against a fixture runtime folder) runs inside WSL - all such tests executing from a native-filesystem staging dir with the shared path used only for one-way copy-in/out; the Windows checkout's git and fossil state is untouched by a WSL-gated run (verifiable: git status/fossil changes identical before and after); suite results on a WSL-less machine are unchanged (skips, not failures); enablement/limitations and the staging pattern recorded in src/tests/AGENTS.md.
### G-062 [achieved 2026-07-11] Canonical project license: BSD-2-Clause LICENSE file with SPDX-identified references → detail: goals/archive/G-062-project-license-file.md
Scope: LICENSE.txt (new, repo root), README.md, punkproject.toml ([project] license field), AGENTS.md (Repo-wide Notes license mention)
Acceptance: LICENSE.txt exists at the repo root containing the standard BSD-2-Clause text with a real copyright line; README.md names BSD-2-Clause and points at LICENSE.txt; root AGENTS.md Repo-wide Notes names the license precisely; punkproject.toml [project] carries license = "BSD-2-Clause"; a repo-sweep for top-level license claims finds none contradicting it (sweep result recorded here).

6
GOALS.md

@ -38,7 +38,7 @@ Every goal has a detail file; it holds the canonical Goal and Acceptance plus an
- Suggesting is always allowed and encouraged when grounded in the work at hand: a discovered gap, a recurring manual step, a deferred design decision, or a natural follow-on that fits what is currently being worked on. Flag it as a candidate goal in conversation or the completion report, optionally with a drafted entry ready for approval. A suggestion is not an edit - nothing is written to this file or `goals/` until the user approves per the proposal-first rule.
- The goal contract spans both tiers: the index entry (title, status tag, Scope) and the detail file's `Goal:` and `Acceptance:` lines.
- Proposal-first: before writing a new goal or changing any contract element in either tier, show the user the proposed wording - the full entry plus detail-file header for a new goal, the changed clause(s) for an edit - and get explicit approval. If the user already supplied or approved the exact wording this session, apply it and report what was written.
- Exception (sanctioned autonomous edit): an agent whose work satisfies a goal's `Acceptance:` (judged against the detail file's criterion, never the index entry alone) must flip that goal to `achieved <date>` as part of its DOX closeout pass, archive it (entry to `GOALS-archive.md` per that file's format, detail file to `goals/archive/`), and report the flip in its completion summary. If the detail file carries a `## Progress` section, the flip additionally requires its remaining-work list to be resolved — empty, or each item verified satisfied; a partial increment never flips a goal.
- Exception (sanctioned autonomous edit): an agent whose work satisfies a goal's `Acceptance:` (judged against the detail file's criterion, never the index entry alone) must flip that goal to `achieved <date>` as part of its DOX closeout pass, archive it (entry to `GOALS-archive.md` per that file's format, detail file to `goals/archive/`), and report the flip in its completion summary. If the detail file carries a `## Progress` section, the flip additionally requires its remaining-work list to be resolved — empty, or each item verified satisfied; a partial increment never flips a goal. The flip records its verification evidence (what was verified, on which kits/runtimes, any remaining manual items) in the detail file's body — the `Status:` line stays bare `achieved <date>`, per the header field grammar in `goals/AGENTS.md`. The flip also includes a reference sweep of the live tier (see `goals/AGENTS.md` Archive rules): pending-tense mentions of the archived goal updated to reflect achievement, and any of its notes actionable for a live goal pointed to from that goal's `## Notes`.
- Agents must not flip `proposed``active`. They flag it in their completion report for the user to confirm.
- Index entries carry no progress: the status tag is the only state the index records. Incremental progress on an `active` goal (what landed, what remains) is recorded in the detail file's `## Progress` section, never as annotations on the index entry.
- Marking a goal `superseded by G-<id>` or `abandoned` includes a test sweep: search the tree (at minimum `src/tests`) for `G-<old id>` references and for the tests named in the goal's detail-file Acceptance, and record each affected test's disposition in the superseding goal's detail file (or the abandoned goal's own): pinned expectations that transfer to the new goal, pins that stand down to plain behaviour characterization, and any that lapse. The sweep never deletes, skips, or weakens a test on its own — that still requires explicit user direction per `src/tests/AGENTS.md`.
@ -262,10 +262,6 @@ Detail: goals/G-060-qemu-test-matrix.md
Scope: test-harness support (location TBD during the work: src/tests/testsupport/ and/or a small punk module), src/tests/shell/ (capability-gated interactive suites), goals/G-061-pseudoconsole-expect.md
Detail: goals/G-061-pseudoconsole-expect.md
### G-062 [proposed] Canonical project license: BSD-2-Clause LICENSE file with SPDX-identified references
Scope: LICENSE.txt (new, repo root), README.md, punkproject.toml ([project] license field), AGENTS.md (Repo-wide Notes license mention)
Detail: goals/G-062-project-license-file.md
### G-063 [proposed] Per-package license tracking: SPDX-normalized indications with copyleft audit
Scope: src/modules (Meta license headers), src/vendormodules/ + src/vendorlib/ (vendored license recording), punk::mix module templates (%license% seeding), mapping module (new, name TBD), audit surface (src/make.tcl or dev commandset - TBD)
Detail: goals/G-063-package-license-tracking.md

24
LICENSE.txt

@ -0,0 +1,24 @@
BSD 2-Clause License
Copyright (c) 2023-2026 Julian Marcel Noble
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this
list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER ``AS IS'' AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

2
README.md

@ -1,6 +1,6 @@
## punkshell - an alternative Tcl Shell
BSD license
BSD-2-Clause license (see `LICENSE.txt`)
Version 0.11.0 (2026-07) — this is **alpha** level software and still highly experimental. See `CHANGELOG.md` for recent changes.

7
goals/AGENTS.md

@ -34,6 +34,8 @@ Goal: <canonical - outcome-focused statement of what done looks like>
Acceptance: <canonical measurable, verifiable pass/fail criterion>
```
Header field grammar (lintable): each header field is one line carrying only its own content - never parenthetical narrative. `Status:` is exactly one of `proposed`, `active`, `abandoned`, `superseded`, or `achieved YYYY-MM-DD`. Verification evidence, design asides, and remaining manual items belong in body sections (`## Progress`, `## Notes`) - prose riding a header field is invisible to restructures that normalise those fields and gets silently dropped (root `AGENTS.md` "Doc Restructures").
Optional sections after the header, only when they have real content:
```
@ -61,6 +63,9 @@ When user-directed work on an `active` goal lands without satisfying its `Accept
### Archive
- `goals/archive/` holds detail files for achieved goals. Archiving happens as part of the achieved flip (see the root `GOALS.md` maintenance rules): the detail file moves `goals/G-<id>-<slug>.md``goals/archive/G-<id>-<slug>.md` and the index entry moves to `GOALS-archive.md`. Do not rename the ID prefix.
- At the achieved flip, record the verification evidence in the detail file body (`## Progress` or `## Notes`): what was verified, on which kits/runtimes, design outcomes worth keeping, and any remaining manual items. The `Status:` line itself carries only `achieved YYYY-MM-DD` (header field grammar above).
- The archive move is content-identical: the file lands in `archive/` byte-for-byte as it left `goals/`, with the flip's content edits (Status line, evidence write-up) made before or after the move so the move diff is a pure rename (root `AGENTS.md` "Doc Restructures").
- Reference sweep at archive time: grep the live tier (`goals/*.md`, `GOALS.md`) for the archived goal's ID. (a) Rewrite pending-tense phrasing ("once G-x lands", "after G-x is achieved") to reflect achievement, with the archive path - e.g. "(G-x, achieved - see goals/archive/G-x-<slug>.md)". (b) For each note in the archived detail file carrying actionable content for a live goal (a follow-on, dependency detail, or design decision that goal will need), add a one-line pointer to that live goal's `## Notes`: "G-x (archived) recorded <thing> - see goals/archive/G-x-<slug>.md". Mentions that are pure history need nothing. Rationale: an archived file is no longer edited, so its forward-pointing insights must be pushed to their targets when it leaves the active set. These are non-contract Notes updates - no pre-approval needed, report in the completion summary.
- No orphan detail files: every file here (excluding `archive/` and this `AGENTS.md`) corresponds to a `proposed`, `active`, `abandoned` or `superseded` entry in root `GOALS.md`; every file under `archive/` corresponds to a record in `GOALS-archive.md`.
## Work Guidance
@ -69,7 +74,7 @@ LF line endings (per root AGENTS.md user preferences).
## Verification
None — this folder is documentation only.
`tclsh scriptlib/developer/goals_lint.tcl` — run after editing goal files or either index. Validates the detail-file header grammar (including the bare-`Status:` rule), index/detail orphan rules in both tiers, and the Status/Scope mirror consistency (plain tclsh, no dependencies; exit 0 clean, exit 1 with one line per finding).
## Child DOX Index

2
goals/G-002-non-nested-subshell.md

@ -67,7 +67,7 @@ Split the subshell launch into layers. The canonical launch API (section 0) is a
## Notes
- Depends on G-001's launch-time console selection. Sequence G-001 before G-002.
- Depends on G-001's launch-time console selection - satisfied: G-001 achieved 2026-07-11 (see goals/archive/G-001-pluggable-console-backends.md), the prerequisite is in place.
- The thread::send -async routing problem is the hardest open question in G-002. The standard Thread package targets the root interp of the receiving thread; the `code` sub-interp is where subshell code actually runs, so async messages miss it. `promise` (vendored at `src/vendormodules/promise-1.2.0.tm`) is the concrete canary — if `promise` works inside a subshell, the routing is correct.
- The TODO at repl:3135-3139 frames the thread-extension investigation with a mainline-acceptance criterion. If that investigation concludes a fork is needed (customised Thread package and/or Tcl patch), create a follow-up goal for the fork's maintenance and upstreaming; do not let it grow inside G-002. The fork itself is out of scope for G-002's acceptance — G-002 is satisfied by whichever routing solution is found, not by the fork existing.
- `tsv::` and `thread::cond` are already used in `codethread-999999.0a1.0.tm` (lines 203-218) for cross-thread result/status handoff. The inter-subshell comms layer should reuse the same primitives rather than introducing a new message bus.

5
goals/G-005-zig-build-infrastructure.md

@ -44,3 +44,8 @@ This goal is the primary mechanism for G-004's outcome. It is independent of and
- The Tcl9-zig experiment is the seed; expect it to need expansion beyond Tcl9 to cover the full dependency set. Don't underestimate this — the experiment covers one component; the goal covers all committed binaries.
- If the zig build for a particular dependency proves infeasible, G-006's download path is the fallback for that dependency, and the goal's acceptance is satisfied by the dependencies it does cover plus download for the rest. Document any such deferral.
- No persisted prior chat on this topic was found in project sessions; the motivation is the user's stated intent and the existing Tcl9-zig experiment.
- G-058 (archived) shipped the static-baseline capture/seeding that zig-built static runtimes
will rely on (boot probe-loads empty-filename [info loaded] entries, seeds ifneeded mappings
into fabricated interps/threads); its detail file records the runtime-shape findings
(tclsfe stub replacement, composite-static exclusion) that a static-runtime build must
respect - see goals/archive/G-058-static-runtime-packages.md.

2
goals/G-008-scoped-console-state.md

@ -71,7 +71,7 @@ them through the owning context.
## Notes
- Sequence after G-007: the journal hooks belong at the same choke points the broker
- Sequence after G-007 (achieved 2026-07-05 - prerequisite in place): the journal hooks belong at the same choke points the broker
instruments, and journaling must catch mutations originating in code interps - exactly what
the broker funnels. Composes with G-002: a subshell targeting a *different* console needs no
scoping (its terminal is its own); this goal covers the common shared-console case.

1
goals/G-010-subshell-tree-navigation.md

@ -49,3 +49,4 @@ escape hatch.
- Suspend/resume semantics for in-flight commands in a suspended subshell (block? continue
headless with output buffered?) is an implementation-time decision to be recorded here when
made; the acceptance only requires suspended subshells to be listable and resumable.
- Archived-goal references in this file: G-007 achieved 2026-07-05 (goals/archive/G-007-console-location-transparency.md).

1
goals/G-011-console-stderr-semantics.md

@ -106,3 +106,4 @@ untouched.
- **Not in scope.** Channel transforms/filtering on err (shellfilter stacks), a separate err
reader, and any change to shellrun's process-layer stdout/stderr handling - this goal defines
the console-side slot they can target.
- Archived-goal references in this file: G-001 achieved 2026-07-11 (goals/archive/G-001-pluggable-console-backends.md);G-007 achieved 2026-07-05 (goals/archive/G-007-console-location-transparency.md).

3
goals/G-012-template-payload-safety.md

@ -87,3 +87,6 @@ payload drift is unmanaged.
- Verification tie-in: the dual-VCS sync checks in `.fossil-settings/AGENTS.md` double as a
regression detector for this class - after this goal, the "git-tracked invisible to fossil"
comparison should show no template-payload entries.
- G-062 (archived) recorded a follow-on in this territory: seeding LICENSE.txt into generated
project layouts (deliberately excluded from G-062's acceptance because src/project_layouts
sync is restricted) - see goals/archive/G-062-project-license-file.md.

4
goals/G-013-raw-mode-default.md

@ -30,8 +30,8 @@ the line-mode type-ahead-loss window during terminal-query raw cycling
gate.
Its startup default should ultimately come from stored configuration via
::punk::config (see G-014) - toml-backed, independently configurable per named
subshell - with off as the built-in fallback. Longer term, once the console
framework is more complete (G-001 backends), this live view could be streamed
subshell - with off as the built-in fallback. Longer term, with the G-001 backend
framework in place (achieved 2026-07-11), this live view could be streamed
off-console instead - e.g. over udp to a monitoring app, or to a tk text-widget
system acting as a pseudo terminal.
3. **console_debugview** (repl ~1976) - already gated (debug_repl > 0).

4
goals/G-014-punk-config-toml.md

@ -60,3 +60,7 @@ tomlish be re-vendored before punk::config implementation proceeds.
- Reusing Tcl-native config formats (plain dict/script files) - rejected: toml is
already the declared format in punk::config 0.1 and is editable/diffable by
users without Tcl knowledge.
## Notes
- Archived-goal references in this file: G-007 achieved 2026-07-05 (goals/archive/G-007-console-location-transparency.md).

3
goals/G-016-projects-work-git-discovery.md

@ -84,6 +84,7 @@ Result-shape considerations:
prevention side (scratch `FOSSIL_HOME` for throwaway repos) is recorded in
root AGENTS.md User Preferences.
- Depends on nothing, but its value to agents is realised through G-015
(reliable piped invocation) and G-017 (agent guidance documenting the call).
(reliable piped invocation - achieved 2026-07-07) and G-017 (agent guidance documenting the call).
- The `-cd` / `-detail` options and case-insensitive glob behaviour of the
existing command are contracts to preserve.
- Archived-goal references in this file: G-049 achieved 2026-07-10 (goals/archive/G-049-punkargs-parse-status-model.md).

4
goals/G-017-agent-project-discovery.md

@ -2,5 +2,5 @@
Status: proposed
Scope: AGENTS.md (root) or a child doc it indexes (guidance content only - no code)
Goal: once G-015 makes piped script calls reliable, repository guidance directs agents asked to locate another local project to query it via a piped `projects.work` call to a punk executable instead of grepping/globbing the wider filesystem.
Acceptance: root AGENTS.md (or a child doc indexed from it) records the exact recommended invocation - executable, subcommand, glob usage, and `-return json` as the parse target with its record fields - and states when filesystem scanning remains appropriate (projects not registered in any discovery source); the guidance is added only after G-015 is achieved (and notes the fossil-only limitation until G-016); following the documented pattern, an agent locates a named sibling project's checkout dir with a single piped call.
Goal: now that G-015 (achieved 2026-07-07 - see goals/archive/G-015-script-subcommand-piped-stdin.md) makes piped script calls reliable, repository guidance directs agents asked to locate another local project to query it via a piped `projects.work` call to a punk executable instead of grepping/globbing the wider filesystem.
Acceptance: root AGENTS.md (or a child doc indexed from it) records the exact recommended invocation - executable, subcommand, glob usage, and `-return json` as the parse target with its record fields - and states when filesystem scanning remains appropriate (projects not registered in any discovery source); the guidance is added only after G-015 is achieved (satisfied - G-015 achieved 2026-07-07; the fossil-only limitation note is still required until G-016); following the documented pattern, an agent locates a named sibling project's checkout dir with a single piped call.

1
goals/G-020-screencap-input-module.md

@ -92,6 +92,7 @@ the module is a separate development, not a refactor of it.
- Related: G-018 (kit composition; Tk as loadable package, no wish binaries),
G-019 (a trimmed capture-capable executable is a plausible scan target),
G-021 (agent-facing surface over this module), G-001 (backend plugin pattern).
- Archived-goal references in this file: G-001 achieved 2026-07-11 (goals/archive/G-001-pluggable-console-backends.md); G-015 achieved 2026-07-07 (goals/archive/G-015-script-subcommand-piped-stdin.md).
## Additional driving use case: repl interactive-behaviour verification (2026-07-11)

72
goals/G-021-agent-visual-verification.md

@ -2,8 +2,8 @@
Status: proposed
Scope: src/modules/punk/ (G-020 module's agent-facing surface), AGENTS.md guidance (post G-015 pattern), src/tests/ (visual-verification test hooks)
Goal: a tool-calling agent can, during a session, use piped script calls (G-015) to a punk executable to locate the applicable UI window, snapshot it to a PNG file and/or base64 output suitable for AI image analysis, and drive mouse/keyboard interactions - enabling tests whose verification is visual-only and/or input-driven.
Acceptance: on Windows, single piped script calls (no interactive session) can: list/match windows for a pattern with machine-parseable output; save a located window's snapshot to a caller-specified path and optionally emit it base64 on stdout; run a scripted interaction sequence (focus, click at offset, type text, snapshot) end-to-end; failures exit nonzero with the error on stderr per G-015 semantics; the invocation patterns are documented for agents alongside the G-017 guidance; at least one real visual-or-input-driven verification (e.g. a Tk app smoke test) is exercised through this path.
Goal: a tool-calling agent can, during a session, use piped script calls (G-015) to a punk executable to locate the applicable UI window, snapshot it, and drive mouse/keyboard interactions - with capture disposition governed by user-configured privacy policy: by default the agent receives an opaque capture token and text-only analysis results relayed to user-approved endpoints, and raw pixel access (file path or base64) only where the user has explicitly enabled it - enabling tests whose verification is visual-only and/or input-driven.
Acceptance: on Windows, single piped script calls (no interactive session) can: list/match windows for a pattern with machine-parseable output; snapshot a located window receiving an opaque capture token by default, with no image path or pixel data in any output; submit a token plus prompt for analysis and receive text-only results, the image going only to an endpoint the user's policy config permits (local-only or allowlist) and a policy-refused endpoint failing nonzero; obtain the raw image (caller-specified PNG path and/or base64 on stdout) only when the user's policy config enables pixel release, the default policy refusing; every capture and disposition is recorded in an audit log; run a scripted interaction sequence (focus, click at offset, type text, snapshot) end-to-end; failures exit nonzero with the error on stderr per G-015 semantics; the invocation patterns and privacy model are documented for agents alongside the G-017 guidance; at least one real visual-or-input-driven verification (e.g. a Tk app smoke test) is exercised through this path.
## Context
@ -22,10 +22,12 @@ tool calls during a session.
`script` subcommand: each operation is a single piped call with
machine-parseable output and G-015 exit-code semantics (nonzero + stderr on
failure, never an interactive shell).
- Operations: window list/match by pattern; snapshot a located window or region
to a caller-specified PNG path; optional base64-on-stdout emission for direct
image ingestion by the calling agent; scripted interaction sequences (focus,
click at offset, key/type, snapshot) composable in one piped script.
- Operations: window list/match by pattern; snapshot a located window or region,
yielding an opaque capture token by default; analysis of a token via
user-approved endpoints returning text only; raw-image release (caller-specified
PNG path, base64-on-stdout) as policy-gated modes per the privacy model below;
scripted interaction sequences (focus, click at offset, key/type, snapshot)
composable in one piped script.
- Windows first, per G-020. Capability introspection flows through, so an agent
on an unsupported platform gets a clean "not supported here" rather than a
hang or crash.
@ -36,6 +38,50 @@ tool calls during a session.
- At least one real verification consumer in src/tests/ or a runbook (e.g. a
Tk app smoke test asserting on a snapshot) proves the loop end-to-end.
## Privacy model (capture disposition)
Captured images are private data from the driven machine; the agent must not get
incidental access to pixels or their storage location. Enforcement lives in the
mechanism, not in AGENTS.md instructions - guidance is an additional layer, not
the gate.
- **Opaque capture tokens**: the snapshot operation returns a token
(e.g. `cap:<random>`), never a path. Images are held by the capture-owning
process (in memory, or in a spool location with randomized names that never
appears in any output). No operation echoes a path or pixel data unless policy
permits it.
- **Analysis relay**: an `analyze` operation takes a token plus a prompt; the
mechanism sends the image to a vision endpoint and returns only the text
result. The endpoint set is user policy: `local-only` (loopback addresses /
named pipes only, e.g. a local vision model) or an explicit allowlist of
specific endpoints. The mechanism resolves and checks the destination itself;
the agent cannot redirect it. A policy-refused endpoint fails nonzero per
G-015 semantics.
- **Pixel-release policy**: a user config setting on the order of
`expose_pixels = never | analyze-only | allow` gates whether the raw image
(file path or base64) can ever be returned to the caller. Default
`analyze-only`.
- **Policy config outside agent-writable scope**: the policy lives in
user-profile configuration - never in the repo, never in AGENTS.md. (A
same-user agent with arbitrary exec could still edit a user-profile file;
see the hardening note for the stronger boundary.)
- **Audit trail**: every capture and every disposition is logged (timestamp,
window matched, image hash, destination endpoint or release mode), so
circumvention is detectable even where it is not preventable.
- **AGENTS.md as an additional layer**: the agent guidance names the punk
capture path as the only sanctioned way to observe GUI state and prohibits
OS-level screenshot one-liners and spool-directory hunting. This keeps a
compliant agent on the private-by-default path; the mechanism plus audit log
handle the rest.
- **Honest enforcement ceiling**: an agent with unrestricted same-user command
execution on the driven machine can capture the screen outside punk entirely;
no in-punk mechanism can prevent that. The design target is: the sanctioned
path is private by default, and circumvention requires deliberate, visible,
auditable steps rather than incidental access. The ceiling rises substantially
when the agent's exec is sandboxed/permission-gated or the agent drives the
machine remotely (no local filesystem access) - the capture interface should
stay transport-agnostic so the remote topology works unchanged.
## Alternatives considered
- OS-level screenshot tooling driven directly by agents (PowerShell/GDI
@ -49,8 +95,18 @@ tool calls during a session.
## Notes
- Depends on G-020 (mechanism) and G-015 (reliable piped calls); guidance
- Depends on G-020 (mechanism) and G-015 (reliable piped calls - achieved 2026-07-07); guidance
placement follows G-017.
- Image handoff sizing: agents ingest images with dimension/size limits;
snapshot operations should support downscale/crop options eventually -
acceptance keeps to save + base64, with sizing noted as a natural follow-on.
acceptance keeps to token + policy-gated release, with sizing noted as a
natural follow-on.
- Privacy hardening beyond this goal's scope - potential down-the-track task
(candidate goal if/when wanted): (a) run the capture broker as a separate
principal (Windows service or second account) with admin-owned policy config,
so reading the spool or editing policy requires elevation rather than mere
same-user access; (b) user consent prompts for pixel release. Design trap for
(b), recorded now because it constrains any future prompt mode: G-020's own
input injection (SendInput) can click an ordinary consent dialog on the driven
desktop - a real consent gate must live where SendInput cannot reach (secure
desktop, UAC-style, or out-of-band confirmation).

5
goals/G-022-fossil-rename-punkshell.md

@ -65,8 +65,9 @@ phantoms surface directly in agent search results.
## Notes
- Dependent on G-015 (reliable piped script calls) for the application step;
the capability work itself has no dependency and can proceed earlier.
- Dependent on G-015 (reliable piped script calls) for the application step -
satisfied: G-015 achieved 2026-07-07, see
goals/archive/G-015-script-subcommand-piped-stdin.md.
- The working-tree directory name (C:/repo/jn/shellspy) is out of scope -
fossil does not require checkout dirs to match the project name.
- Other checkouts of shellspy.fossil (e.g. a shellspy_copy working tree, if

5
goals/G-024-mapvfs-toml.md

@ -64,3 +64,8 @@ with the G-014 direction of toml + tomlish for declared configuration.
punk::config consumption; build-tooling use here can proceed with tomlish
as-is), G-012 (layout template refresh, if layout copies of runtime config
are affected).
- G-037 (archived) named this file as the natural home for per-kit vendorlib payload
declarations, and recorded that vendorlib and kit vfs trees have drifted in BOTH directions
(vendorlib newer for udp, older for twapi) so any sync declaration needs explicit
direction/authority semantics, not "copy newer over older" - see
goals/archive/G-037-vendorlib-vfs-propagation.md.

1
goals/G-025-exe-selfreport.md

@ -81,3 +81,4 @@ Edge cases driving the acceptance clauses:
- Related: G-023 (version-named binaries this complements), G-024 (build
mapping that will drive stamping inputs), G-015 (piped invocation of the
subcommand), G-014 (punkproject.toml reading may share tomlish plumbing).
- Archived-goal references in this file: G-007 achieved 2026-07-05 (goals/archive/G-007-console-location-transparency.md);G-015 achieved 2026-07-07 (goals/archive/G-015-script-subcommand-piped-stdin.md).

3
goals/G-027-derived-project-pull-updates.md

@ -118,3 +118,6 @@ What a pull may touch splits by ownership, not by file type:
and the 2026-07-06 manual recovery are the concrete push-model precedents to
test the pull against: a scratch project seeded at an old layout vintage
should be fully recoverable by one pull.
- G-062 (archived) recorded a follow-on relevant here: seeding LICENSE.txt into generated
project layouts and pulling it into derived projects (deliberately excluded from G-062's
acceptance) - see goals/archive/G-062-project-license-file.md.

1
goals/G-030-maketcl-punkargs.md

@ -74,3 +74,4 @@ usage as its first surface.
- Observation parked here from the same session: runtests/make.tcl-adjacent
tooling that returns result dicts should document their key shape (see the
runx -tcl exitcode note in G-015's detail).
- Archived-goal references in this file: G-015 achieved 2026-07-07 (goals/archive/G-015-script-subcommand-piped-stdin.md).

9
goals/G-031-componentized-kit-boot.md

@ -71,10 +71,11 @@ permanently own - a thousand lines of boot code.
## Notes
- Sequencing with G-015: the script-subcommand work touches the same dispatch
tail. Either order works, but whichever lands second must preserve the
other's behaviour; if G-015 proceeds first on the monolith, its dispatch
logic moves into the boot core here.
- Sequencing with G-015: resolved - G-015 proceeded first on the monolith
(achieved 2026-07-07 as the dedicated app-punkscript package, see
goals/archive/G-015-script-subcommand-piped-stdin.md), so its dispatch
logic moves into the boot core here and this goal must preserve the
script subcommand's behaviour.
- Related: G-027 (ownership + pull channel this is designed for), G-025
(boot-core vintage reporting), G-032 (punk::args-defined dispatch built on
the registration model), G-012 (layout payload hygiene), G-018 (plain tclsh

1
goals/G-032-launcher-punkargs.md

@ -62,3 +62,4 @@ in `-help` by registration alone.
natural first parsed subcommand; exit-code semantics), G-025 (version-report
subcommand born punk::args-defined), G-013 (the debugrepl activation review
mentions proper-command conversions that would slot into the same pattern).
- Archived-goal references in this file: G-015 achieved 2026-07-07 (goals/archive/G-015-script-subcommand-piped-stdin.md).

1
goals/G-033-proj-mode-cwd-project.md

@ -170,3 +170,4 @@ ships with the prefix, not before):
packagemode help text above), G-015 (`proj:... script` also benefits),
G-027 (if a universal project marker is ever seeded, proj could prefer it
over the structural VCS heuristic).
- Archived-goal references in this file: G-015 achieved 2026-07-07 (goals/archive/G-015-script-subcommand-piped-stdin.md).

1
goals/G-038-piped-session-continuity.md

@ -181,3 +181,4 @@ object's channels). State observed pre-fix, for reference: repl thread post-rest
`stdin` eof=1 (the exhausted pipe) alongside the new `file...` CONIN$ channel; the code
interp's stdin is a live console channel (utf-16, -inputmode present); anchored instances
known to the repl thread: `default` (anchored to the dead pair).
- Archived-goal references in this file: G-001 achieved 2026-07-11 (goals/archive/G-001-pluggable-console-backends.md);G-007 achieved 2026-07-05 (goals/archive/G-007-console-location-transparency.md);G-015 achieved 2026-07-07 (goals/archive/G-015-script-subcommand-piped-stdin.md);G-036 achieved 2026-07-08 (goals/archive/G-036-tcl9-udp-console-worker-wedge.md).

1
goals/G-039-orphan-console-spin.md

@ -46,3 +46,4 @@ punk::console query/read helpers.
same neighbourhood).
- Harness hygiene until fixed: verify no stray punk902z processes before scoring any
liveness-based test run.
- Archived-goal references in this file: G-036 achieved 2026-07-08 (goals/archive/G-036-tcl9-udp-console-worker-wedge.md).

1
goals/G-041-punkargs-form-matching.md

@ -84,3 +84,4 @@ when shaping step 1.
assignment (punk/args ~l.6186; its happy-path twin was already commented). Fixed
2026-07-08 (punk::args 0.2.3) - noted here because form-attempt logic will exercise
that failure path heavily.
- Archived-goal references in this file: G-040 achieved 2026-07-08 (goals/archive/G-040-punkargs-choicealiases.md).

5
goals/G-042-subshell-help-topics.md

@ -48,9 +48,8 @@ explicit rather than accidental.
escape hatch for a shadowed command name is 'i <cmd>' / 's <cmd>' (the first-class
command-help route) - document it where the topic set is presented ('help topics'
footer or the ::punk::help -help text).
4. Display: alias folding for declared topics inherits whatever G-040 lands
(-choicealiases); until then aliases appear as separate documented choices, as the
built-ins do today.
4. Display: alias folding for declared topics inherits G-040's -choicealiases
(achieved 2026-07-08 - see goals/archive/G-040-punkargs-choicealiases.md).
## Relationship to the intended plugin facility (code review 2026-07-08)

3
goals/G-044-repl-command-completion.md

@ -38,7 +38,7 @@ arginfo principle: introspection must not run commands to elicit usage).
plus the synopsis/form information for the current position. Sources:
punk::ns::cmdinfo for the command-word path, punk::args::get_spec for the argument
position. Choice-word matching must reuse the parser's resolution rules (the shared
choice-resolution helper planned in G-040 is the natural common code; prefix
choice-resolution helper from G-040 (achieved 2026-07-08) is the natural common code; prefix
highlighting from the usage renderer already computes minimal prefixes).
Form handling: before G-041, enumerate all forms' next-position candidates (union)
and show all/form-0 synopses with the limitation noted; when G-041's candidacy API
@ -86,6 +86,7 @@ arginfo principle: introspection must not run commands to elicit usage).
src/tests/modules/punk/ns/testsuites/ns/cmdflow.test (2026-07-08), including ensemble
-parameters handling; the choice semantics by
src/tests/modules/punk/args/testsuites/args/choices.test; forms by forms.test.
- Archived-goal references in this file: G-001 achieved 2026-07-11 (goals/archive/G-001-pluggable-console-backends.md); G-040 achieved 2026-07-08 (goals/archive/G-040-punkargs-choicealiases.md).
## Repl behaviour preserve-list + testability findings (2026-07-11, user-directed - pre-refactor ordering)

1
goals/G-045-punkargs-authoring-ergonomics.md

@ -94,3 +94,4 @@ punk::imap4's {\Deleted}/{$MDNSent} choice values) have them stated, not just te
- Session records 2026-07-09: probe scripts renderprobe*.tcl (scratchpad), the user's
experimental define_docs hack (uncommitted punk.tm working-tree diff at the time of
drafting - superseded by this goal when implemented).
- Archived-goal references in this file: G-046 achieved 2026-07-10 (goals/archive/G-046-punkargs-deferred-help-and-fixes.md).

2
goals/G-050-synopsis-validity-marking.md

@ -37,7 +37,7 @@ string").
## Notes
- Depends on G-049 (status structure). G-044's hint display is a future consumer of the
- Depends on G-049 (status structure) - satisfied: achieved 2026-07-10, see goals/archive/G-049-punkargs-parse-status-model.md. G-044's hint display is a future consumer of the
same API - keep the marking function reusable outside synopsis/cmdhelp rendering.
- Existing string-shape tests: args testsuite synopsis.test and the installed
test::punk::args synopsis tests ("must be no leading space" convention) - marking must

1
goals/G-052-oo-method-autodef.md

@ -53,3 +53,4 @@ just not wired up as a general method-level (autodef) generation step in the doc
- Filters, private methods and `unknown` handlers are marked in choiceinfo today but out
of scope for autodef generation.
- Coordinate the cmdtype reported for method resolutions with G-051's doconly decision.
- Archived-goal references in this file: G-040 achieved 2026-07-08 (goals/archive/G-040-punkargs-choicealiases.md).

1
goals/G-053-punkargs-multiple-ranges.md

@ -66,3 +66,4 @@ rendering gain meaningful bounded-repetition forms (e.g. "0-1", "2-4").
canonicalization must not regress the hot path).
- The runtests `-include-paths` fix (repeatable, accumulate, single-list form still
accepted) shipped independently on 2026-07-10 and does not depend on this goal.
- Archived-goal references in this file: G-046 achieved 2026-07-10 (goals/archive/G-046-punkargs-deferred-help-and-fixes.md).

1
goals/G-055-tclcore-regen-workflow.md

@ -90,3 +90,4 @@ other commands including the multi-form ::after.
side-effect-free invocations of pure builtins (deliberately-invalid calls
harvesting error metadata). This does not relax cmdhelp's rule of never calling
arbitrary commands to elicit usage at query time.
- Archived-goal references in this file: G-049 achieved 2026-07-10 (goals/archive/G-049-punkargs-parse-status-model.md);G-054 achieved 2026-07-11 (goals/archive/G-054-tclcore-stringis-harvest.md).

1
goals/G-056-punkargs-word-wrapping.md

@ -56,3 +56,4 @@ the current table -maxwidth 80 default; an explicit width option overrides both.
those, not just the table path.
- Characterization first: pin current no-wrap rendering of an over-width line
(overflow behaviour) before changing it, per the usual GAP-pin workflow.
- Archived-goal references in this file: G-046 achieved 2026-07-10 (goals/archive/G-046-punkargs-deferred-help-and-fixes.md).

1
goals/G-060-qemu-test-matrix.md

@ -61,3 +61,4 @@ direction rather than a new ad-hoc location.
exercises runtime.cmd's unix payload for real), [[G-059]] (contract sibling).
- Performance expectations should stay modest (emulated arm will be slow); the matrix
is for correctness verification, not speed benchmarking.
- Archived-goal references in this file: G-059 achieved 2026-07-11 (goals/archive/G-059-wsl-test-driving.md).

1
goals/G-061-pseudoconsole-expect.md

@ -77,3 +77,4 @@ expect-like primitives over a REAL pseudo-terminal:
- ConPTY availability gates the windows side to Win10 1809+ (fine for punkshell's
supported baseline); the capability probe should verify actual ConPTY function, not
windows version.
- Archived-goal references in this file: G-001 achieved 2026-07-11 (goals/archive/G-001-pluggable-console-backends.md);G-059 achieved 2026-07-11 (goals/archive/G-059-wsl-test-driving.md).

1
goals/G-063-package-license-tracking.md

@ -32,3 +32,4 @@ it auditable per package.
- Related: G-026 (vendorupdate is the natural hook for capturing vendored license
provenance), G-060 (recorded GPL-safe posture), G-062 (project's own license id),
G-064 (lib.search as the interactive surface for these indications).
- Archived-goal references in this file: G-062 achieved 2026-07-11 (goals/archive/G-062-project-license-file.md).

11
goals/archive/G-015-script-subcommand-piped-stdin.md

@ -95,6 +95,17 @@ punk_main.tcl's `punk` subcommand case).
## Notes
- Achieved-status detail (restored 2026-07-11; this text originally rode the
Status: line as a parenthetical and was dropped when the 2026-07-11 two-tier
index restructure normalised detail-file Status lines to the bare index form):
implemented as src/lib/app-punkscript; verified on punk902z/tcl9 and
punksys/tcl8.6 kits: piped success/error/exit-code cases, file form with args
and stdin passthrough, and the motivating `dev projects.work *tomlish*`
one-liner - table emitted, exit 0, no boilerplate. Stdin form additionally
echoes the script's final result when non-empty, giving return-value commands
one-liner ergonomics; file form keeps pure script semantics.
Terminal-stdin-with-no-args yields a usage error by design - spot-check from a
real console typed session remains a manual item.
- Result-shape observation (2026-07-07): `shellrun::runx -tcl` returns a dict
keyed {result stdout stderr} with NO exitcode key, unlike the exec-mode
result (exitinfo with exitcode) - callers doing `dict get $result exitcode`

19
goals/G-062-project-license-file.md → goals/archive/G-062-project-license-file.md

@ -1,6 +1,6 @@
# G-062 Canonical project license: BSD-2-Clause LICENSE file with SPDX-identified references
Status: proposed
Status: achieved 2026-07-11
Scope: LICENSE.txt (new, repo root), README.md, punkproject.toml ([project] license field), AGENTS.md (Repo-wide Notes license mention)
Goal: the repository declares exactly one canonical license: a root LICENSE.txt containing the verbatim BSD-2-Clause text, with README.md and root AGENTS.md naming the license by its SPDX identifier and pointing at LICENSE.txt, and punkproject.toml carrying the identifier machine-readably so tooling (the G-063 audit, derived-project seeding) can read the project license without parsing prose.
Acceptance: LICENSE.txt exists at the repo root containing the standard BSD-2-Clause text with a real copyright line; README.md names BSD-2-Clause and points at LICENSE.txt; root AGENTS.md Repo-wide Notes names the license precisely; punkproject.toml [project] carries license = "BSD-2-Clause"; a repo-sweep for top-level license claims finds none contradicting it (sweep result recorded here).
@ -14,8 +14,21 @@ a variant is not a specific license; BSD-2-Clause is the chosen variant
## Notes
- Copyright line content (holder name, year range) to be confirmed with the user at
write time.
- Copyright holder: Julian Marcel Noble; year range: 2023-2026 (user-confirmed
2026-07-11).
- Seeding LICENSE.txt into generated project layouts is a natural follow-on but is
deliberately not part of this goal's acceptance (src/project_layouts sync is
restricted; see G-012/G-027 territory).
## Sweep result (2026-07-11)
Repo-sweep for top-level license claims found none contradicting BSD-2-Clause:
- `README.md`, `AGENTS.md`, `punkproject.toml` — now explicitly BSD-2-Clause (this goal's edits).
- `src/embedded/md/doc/files/punk/_module_cap-0.1.0.tm.md` — "Copyright © 2023 JMNoble - BSD licensed" is a module-level generated doc, not a project-level claim; the module author's own license indication, not the project's.
- `src/vfs/punk9win.vfs/lib/materialicons0.2/README.md` — "License: BSD" — vendored library (materialicons), not the project license.
- `src/vfs/*/tdot0.3.1/tdot.md` — "MIT License" — vendored library (tdot), its own license.
- `src/vfs/critclxxx.vfs/embedded/md/files/critcl_license.md` — "All packages are under the BSD license" — vendored critcl documentation, not the project's claim.
- Goal files (G-060, G-063, G-064) reference "license" in their contracts — descriptive, not claiming a project license.
No contradictions. The project-level license declaration is unambiguous: BSD-2-Clause via `LICENSE.txt`, `README.md`, `AGENTS.md`, and `punkproject.toml`.

3
punkproject.toml

@ -1,3 +1,4 @@
[project]
name = "punkshell"
version = "0.11.0"
version = "0.12.1"
license = "BSD-2-Clause"

218
scriptlib/developer/goals_lint.tcl

@ -0,0 +1,218 @@
#!/usr/bin/env tclsh
# goals_lint.tcl - lint the two-tier goals documentation
# (root GOALS.md / GOALS-archive.md indexes + goals/ / goals/archive/ detail files)
#
# Validates the contracts declared in goals/AGENTS.md and root AGENTS.md:
# - detail-file header grammar: '# G-<id> <title>' then a blank line, then
# Status:/Scope:/Goal:/Acceptance: exactly once each, in order, one line each,
# before any '## section'; no prose riding a header field
# - Status: values are exactly one of proposed|active|abandoned|superseded or
# 'achieved YYYY-MM-DD'
# - active-tree detail files are not 'achieved'; archive detail files are
# - no orphans: every goals/*.md detail file has a GOALS.md index entry and vice
# versa; every goals/archive/*.md file has a GOALS-archive.md record and vice versa
# - Status:/Scope: mirror lines in detail files match the owning index entry
#
# Plain tclsh (8.6+ or 9), no package dependencies - runnable from any checkout:
# tclsh scriptlib/developer/goals_lint.tcl [repo-root]
# Exit 0 when clean; exit 1 with one 'path: problem' line per finding.
set root [expr {$argc >= 1 ? [lindex $argv 0] : [file dirname [file dirname [file dirname [file normalize [info script]]]]]}]
if {![file isfile [file join $root GOALS.md]] || ![file isdir [file join $root goals]]} {
puts stderr "goals_lint: '$root' does not look like the punkshell repo root (need GOALS.md and goals/)"
exit 2
}
set findings {}
proc flag {path msg} {
global findings
lappend findings "$path: $msg"
}
set status_re {^(proposed|active|abandoned|superseded|achieved \d{4}-\d{2}-\d{2})$}
proc read_lines {path} {
set f [open $path r]
fconfigure $f -encoding utf-8
set lines [split [read $f] \n]
close $f
return $lines
}
# --- parse an index file (GOALS.md or GOALS-archive.md) ---------------------
# returns dict: id -> {status <s> title <t> scope <scope-or-empty>}
proc parse_index {path} {
global status_re
set entries [dict create]
set relpath [file tail $path]
set curid ""
foreach line [read_lines $path] {
if {[regexp {^### G-(\d+) \[([^\]]*)\] (.*)$} $line -> id status title]} {
set curid $id
if {[dict exists $entries $id]} {
flag $relpath "duplicate index entry G-$id"
}
if {![regexp $status_re $status]} {
flag $relpath "G-$id: bad status '\[$status\]' (want proposed|active|abandoned|superseded|achieved YYYY-MM-DD)"
}
dict set entries $id [dict create status $status title $title scope ""]
} elseif {[regexp {^### } $line]} {
set curid ""
} elseif {$curid ne "" && [regexp {^Scope: (.*)$} $line -> scope]} {
dict set entries $curid scope $scope
}
}
return $entries
}
# --- lint one detail file ----------------------------------------------------
# returns dict {id <id> status <s> scope <s>} (empty strings when unparseable)
proc lint_detail {path relpath} {
global status_re
set lines [read_lines $path]
set fname [file tail $path]
set result [dict create id "" status "" scope ""]
if {![regexp {^G-(\d+)-[a-z0-9-]+\.md$} $fname -> fid]} {
flag $relpath "filename does not match G-<id>-<slug>.md"
set fid ""
}
dict set result id $fid
if {![regexp {^# G-(\d+) .+$} [lindex $lines 0] -> hid]} {
flag $relpath "line 1 is not '# G-<id> <title>'"
set hid ""
}
if {$fid ne "" && $hid ne "" && $hid ne $fid} {
flag $relpath "title-line id G-$hid does not match filename id G-$fid"
}
if {[lindex $lines 1] ne ""} {
flag $relpath "line 2 is not blank"
}
# header fields: Status/Scope/Goal/Acceptance exactly once each, in order,
# before the first '## ' section
set want {Status Scope Goal Acceptance}
set found [dict create]
set order {}
set i 2
foreach line [lrange $lines 2 end] {
incr i
if {[regexp {^## } $line]} break
if {[regexp {^(Status|Scope|Goal|Acceptance): ?(.*)$} $line -> field value]} {
if {[dict exists $found $field]} {
flag $relpath "duplicate header field '$field:' (line $i)"
} else {
dict set found $field $value
lappend order $field
}
if {$value eq ""} {
flag $relpath "empty header field '$field:' (line $i)"
}
}
}
foreach field $want {
if {![dict exists $found $field]} {
flag $relpath "missing header field '$field:'"
}
}
if {[llength $order] == 4 && $order ne $want} {
flag $relpath "header fields out of order ([join $order /] - want [join $want /])"
}
if {[dict exists $found Status]} {
set status [dict get $found Status]
dict set result status $status
if {![regexp $status_re $status]} {
flag $relpath "bad Status line 'Status: $status' - must be exactly proposed|active|abandoned|superseded or 'achieved YYYY-MM-DD' with no additional prose (verification evidence belongs in ## Progress / ## Notes)"
}
}
if {[dict exists $found Scope]} {
dict set result scope [dict get $found Scope]
}
return $result
}
# --- run ----------------------------------------------------------------------
set active_index [parse_index [file join $root GOALS.md]]
set archive_index [parse_index [file join $root GOALS-archive.md]]
foreach {dir index indexname achieved_expected} [list \
goals $active_index GOALS.md 0 \
[file join goals archive] $archive_index GOALS-archive.md 1] {
set seen [dict create]
foreach path [lsort [glob -nocomplain -directory [file join $root $dir] G-*.md]] {
set relpath [string map {\\ /} [file join $dir [file tail $path]]]
set info [lint_detail $path $relpath]
set id [dict get $info id]
if {$id eq ""} continue
dict set seen $id 1
set achieved [string match "achieved *" [dict get $info status]]
if {$achieved_expected && !$achieved && [dict get $info status] ne ""} {
flag $relpath "archived detail file Status is '[dict get $info status]' - archive/ holds achieved goals only"
} elseif {!$achieved_expected && $achieved} {
flag $relpath "achieved goal still in goals/ - the achieved flip includes archiving (move to goals/archive/, record in GOALS-archive.md)"
}
if {![dict exists $index $id]} {
flag $relpath "orphan detail file - no G-$id entry in $indexname"
continue
}
set entry [dict get $index $id]
if {[dict get $info status] ne "" && [dict get $info status] ne [dict get $entry status]} {
flag $relpath "Status mirror '[dict get $info status]' disagrees with $indexname '\[[dict get $entry status]\]' (index wins)"
}
if {[dict get $info scope] ne "" && [dict get $entry scope] ne "" \
&& [dict get $info scope] ne [dict get $entry scope]} {
flag $relpath "Scope mirror disagrees with $indexname (index wins)"
}
}
foreach id [dict keys $index] {
if {![dict exists $seen $id]} {
flag $indexname "G-$id has no detail file in $dir/"
}
}
}
# --- informational: candidate stale references to archived goals ---------------
# An active detail file mentioning an archived goal with no achieved/archive/landed
# marker on ANY of the mentioning lines has likely missed the archive-time reference
# sweep (goals/AGENTS.md Archive rules) - a reader cannot tell the referenced goal is
# done. One marked mention per file suffices; pure-history mentions on other lines
# are fine. Heuristic - reported as warnings, never fatal.
set warnings {}
foreach path [lsort [glob -nocomplain -directory [file join $root goals] G-*.md]] {
set relpath goals/[file tail $path]
set mentioned [dict create] ;# id -> 1 when any mentioning line carries a marker
set lines [read_lines $path]
set n [llength $lines]
for {set i 0} {$i < $n} {incr i} {
set line [lindex $lines $i]
# marker may sit on the mention line or a wrapped neighbour
set window "$line [lindex $lines [expr {$i-1}]] [lindex $lines [expr {$i+1}]]"
set marked [regexp -nocase {achieved|archive|landed} $window]
foreach id [regexp -all -inline {G-\d+} $line] {
set num [string range $id 2 end]
if {![dict exists $archive_index $num]} continue
if {$marked || ![dict exists $mentioned $num]} {
dict set mentioned $num $marked
}
}
}
dict for {num marked} $mentioned {
if {!$marked} {
lappend warnings "$relpath: references archived goal G-$num with no achieved/archive marker anywhere - reference sweep candidate (goals/AGENTS.md Archive rules)"
}
}
}
if {[llength $warnings]} {
puts stderr "goals_lint: [llength $warnings] informational warning(s):"
puts stderr [join $warnings \n]
}
if {[llength $findings]} {
puts [join $findings \n]
puts stderr "goals_lint: [llength $findings] finding(s)"
exit 1
}
puts "goals_lint: clean ([dict size $active_index] active-index goals, [dict size $archive_index] archived)"
exit 0

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

@ -905,6 +905,13 @@ tcl::namespace::eval punk::args {
-choices {<choicelist>}
A list of allowable values for an argument.
The -default value doesn't have to be in the list.
Synopsis display: a choice set of 1 to 3 members (counting
-choicegroups members) with -choicerestricted true displays
in the synopsis as unitalicised literal alternates
(e.g cancel, or left|centre|right) in the same style as
literal()/literalprefix() type-alternatives. Larger or
unrestricted choice sets display as the italicised argument
name or type, and a -typesynopsis always takes precedence.
If a -type is specified - it doesn't apply to choice members.
It will only be used for validation if the -choicerestricted
option is set to false. If all choices are specified in values
@ -11598,6 +11605,34 @@ tcl::namespace::eval punk::args {
}
proc private::synopsis_choice_literals {arginfo} {
#Small restricted choice sets display in synopses as unitalicised literal alternates,
#matching the display style of literal()/literalprefix() type-alternatives.
#Returns a list of list-protected choice words, or an empty list when the rule doesn't apply:
# - more than 3 choices (reader should refer to the full help for the choice list)
# - -choicerestricted 0 (other values are also acceptable - bare literals would be misleading)
#A defined -typesynopsis always takes precedence - callers consult it before this.
set allchoices [list]
if {[dict exists $arginfo -choices]} {
set allchoices [dict get $arginfo -choices]
}
foreach {groupname members} [Dict_getdef $arginfo -choicegroups {}] {
lappend allchoices {*}$members
}
set allchoices [punk::args::lib::lunique $allchoices]
if {[llength $allchoices] == 0 || [llength $allchoices] > 3} {
return [list]
}
if {![Dict_getdef $arginfo -choicerestricted 1]} {
return [list]
}
set literals [list]
foreach c $allchoices {
lappend literals [list $c]
}
return $literals
}
proc private::synopsis_form_arg_display {formdict argname} {
#non-colour SGR such as bold/italic/strike - so we don't need to worry about NOCOLOR settings
set I "\x1b\[3m" ;#[punk::ansi::a+ italic]
@ -11624,6 +11659,12 @@ tcl::namespace::eval punk::args {
} else {
set tp_displaylist [lrepeat [llength $typelist] ""]
}
if {[llength $typelist] == 1} {
set choice_literals [private::synopsis_choice_literals $arginfo]
} else {
#choice semantics for multi-element clauses are per-clause - keep name/type display hints
set choice_literals [list]
}
foreach typespec $typelist td $tp_displaylist elementname $name_tail {
#elementname will commonly be empty after first iteration
@ -11663,7 +11704,9 @@ tcl::namespace::eval punk::args {
#and if there are enough tail words in the argname to match the position in the type list
#empty strings can be put in -typesynopsis positions to only override the type information for certain elements of the clause
#- e.g for a type list of {string int} we could specify a typesynopsis of {"" "count"} to get display of "FILENAME count" for an argname of "file FILENAME FILECOUNT"
if {[llength $name_tail] >= [llength $typelist]} {
if {[llength $choice_literals]} {
lappend alternates {*}$choice_literals
} elseif {[llength $name_tail] >= [llength $typelist]} {
#important to list protect $elementname e.g look at ::apply
#The name may contain spaces e.g "{args body ?namespace?}"
#This must not be split into multiple words - it is a single element name that happens to contain spaces.
@ -11696,13 +11739,6 @@ tcl::namespace::eval punk::args {
set display "\[$clause\]..."
} else {
set display "\[$clause\]"
#if {[dict exists $arginfo -choices] && [llength [dict get $arginfo -choices]] == 1} {
# set display "?[lindex [dict get $arginfo -choices] 0]?"
#} elseif {[dict get $arginfo -type] eq "literal"} {
# set display "?$argname?"
#} else {
# set display "?$I$argname$NI?"
#}
}
} else {
if {[dict get $arginfo -multiple]} {
@ -11710,13 +11746,6 @@ tcl::namespace::eval punk::args {
set display "$clause \[$clause\]..."
} else {
set display $clause
#if {[dict exists $arginfo -choices] && [llength [dict get $arginfo -choices]] == 1} {
# set display "[lindex [dict get $arginfo -choices] 0]"
#} elseif {[dict get $arginfo -type] eq "literal"} {
# set display $argname
#} else {
# set display "$I$argname$NI"
#}
}
}
return $display
@ -11885,6 +11914,7 @@ tcl::namespace::eval punk::args {
} else {
set alternates [list];#alternate acceptable types e.g literal(yes)|literal(ok) or indexpression|literal(first)
set choice_literals [private::synopsis_choice_literals $arginfo]
set type_alternatives [private::split_type_expression $tp]
foreach tp_alternative $type_alternatives {
set match [lindex $tp_alternative 1]
@ -11902,7 +11932,11 @@ tcl::namespace::eval punk::args {
lappend alternates [list *$match]
}
default {
lappend alternates $I<$tp_alternative>$NI
if {[llength $choice_literals]} {
lappend alternates {*}$choice_literals
} else {
lappend alternates $I<$tp_alternative>$NI
}
}
}
}

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

@ -1,6 +1,7 @@
0.5.0
0.6.0
#First line must be a semantic version number
#all other lines are ignored.
#0.6.0 - synopsis display: small restricted choice sets now render as literal alternates - an argument whose choice pool (-choices plus -choicegroups members, deduplicated) has 1-3 members and -choicerestricted true (the default) displays those words unitalicised joined by | in synopses (e.g 'after cancel' shows literal cancel; a 3-choice option shows (left|centre|right)), matching the display style of literal()/literalprefix() type-alternatives. Larger or unrestricted (-choicerestricted 0) choice sets keep the italicised argname/<type> display, and an explicit -typesynopsis always takes precedence. New private helper punk::args::private::synopsis_choice_literals shared by both synopsis render paths (leaders/values via synopsis_form_arg_display, options inline in synopsis); applies only to single-element -type lists (multi-element clause display unchanged). define doc for -choices documents the rule. Tests: synopsis.test - new characterization coverage for literal/literalprefix/stringstartswith/stringendswith type-alternates, option alternate parenthesization, multi-element clause display, -typesynopsis (value element lists, option passthrough incl documenter ANSI), plus the new choice-literal rule (small sets in leader/option/value positions, choicegroups counting, >3 and unrestricted fallbacks, -typesynopsis precedence)
#0.5.0 - G-049 parse-status data model: new punk::args::parse_status - runs a parse attempt (withid/withdef) and returns a documented status structure instead of raising on validation failure (overall ok/status valid|invalid|incomplete/scheme/message/errorcode-minus-argspecs/failureclass/badarg/id/form/receivednames + per-argument argstatus with class/status ok|bad|unparsed/received/positions/hasvalue/value-in-effect incl -default fill). arg_error: new -parsestatus option - both renderers (table and string) now derive goodarg/badarg row marking and choice value-in-effect highlighting from the structure (built internally from -badarg/-parsedargs when not supplied), replacing the transient goodargs/badarg locals; scheme colours resolve per-render into a local array (scheme renders no longer mutate the shared arg_error_CLR array - the -nocolour leak) and the DOCUMENTED -scheme choice value 'nocolour' (and 'nocolor') now takes effect instead of falling through to leftover colours. parse: new -caller option overriding the %caller% frame-walk substitution in validation failure messages (parse_status defaults it to the definition's @cmd -name). get_dict: missingrequiredvalue/missingrequiredleader allocation failures now carry -badarg (type-failed words get badarg marking, not just choice violations). Tests: parsestatus.test (new), usagemarking.test nocolour/leak GAP pins flipped + -parsestatus render parity
#0.4.2 - G-046: display-field deferral - resolve no longer tstr-expands display-only content during argument resolution: ${...} in -help (@cmd/@examples/argument records) and @formdisplay -header/-body is masked with inert tokens (spec key DISPLAY_DEFERRED) and expanded on demand at display time (arg_error/eg/resolved_def/@default-copyfrom hooks; separate argdefcache_display cache for non-dynamic defs; @dynamic display content re-expands per render preserving provider refresh). First parse of heavily documented commands drops accordingly (punk::ansi::mark_columns ~4.3s -> ~12ms; tclcore ::lseq resolve ~184ms -> ~2ms) and -help content that calls punk::args-parsing commands (including against its own id) no longer stalls or loops - plus a display-time reentrancy guard (raw ${...} source substituted on nested expansion of the same id). Record splitter factored to private::split_definition_records. Also: @dynamic second-round multiline substitutions into deferred fields now get the 'line' paramindents alignment (rendering_atdynamic_multiline_help_insertion GAP flipped); prefix/alias choice normalization writeback no longer list-quotes single-element-clause values ({\Deleted} shape bug, choicegroups_imap_prefix_shape GAP flipped); -return string renderer aligns cmd-help continuations under the Description label and its Example line shows the example instead of the doc url. Tests: deferredhelp.test (new), rendering.test/choicegroups.test updated per G-046 acceptance
#0.4.1 - fixed ensemble_subcommands_definition building the definition against unloaded argdocs: subhelp/doctype-punkargs choiceinfo entries and synopsis choicelabels were omitted for documented subcommands whose registered namespace (::punk::args::register::NAMESPACES) had not yet been lazily loaded - e.g first 'i ansistring' in a fresh shell rendered the autogenerated ensemble help without subcommand-help markers until something else loaded ::punk::ansi::ansistring. The generator now runs update_definitions for the namespaces its id_exists checks could resolve in before testing them (tests: punk/args ensembledef.test, punk/ns cmdhelp.test cmdhelp_ensemble_lazy_registered_argdocs)

2
src/tests/modules/AGENTS.md

@ -40,7 +40,7 @@ Unit tests for editable source modules under `src/modules/`, `src/modules_tcl8/`
- `opunk/console/` — ::opunk::Console backend subclass tests (`testsuites/console/backends.test`, G-001): virtual dispatch of subclass overrides through base-class calls and punk::console::console_spec_resolve (both unchanged), TestConsole determinism + probe-free at_eof, SshConsole capability/eof + the flagship size-via-ANSI-query-over-socket case (a scripted remote terminal answers CSI 6n), TkConsole widget size/eof (gated behind env PUNK_TEST_TK=1 - Tk in the shared testinterp has side effects; also verifiable standalone under a tk-capable kit e.g `punk91 src <script>`)
- `punkcheck/` — punkcheck module tests (install, summarize_install_resultdict, installtrack)
- `punk/ansi/` — punk::ansi tests (`testsuites/ansi/`): ansistrip/ansimerge, plus characterization of the ANSI-at-position mechanisms (`ansistring.test`: INDEX/INDEXCODE/INDEXCHAR/RANGE/INSERT grapheme indexing with SGR-prefix merging, INDEXCOLUMNS/COLUMNINDEX double-wide column mapping, trim/VIEW), code splitting invariants (`ta.test`: detect/detectcode distinction, split_codes/split_codes_single/split_at_codes shapes and round-trip) and single-code/effective-state semantics (`codetype.test`: is_sgr_reset/has_sgr_leadingreset, has_any/all_effective, sgr_merge, sequence_type classify). ANSI codes in these tests are literal escape strings so results are colour-state independent
- `punk/args/` — punk::args tests (`testsuites/args/`): parsing, choices/choicegroups, forms, rendering/indentation characterization, usage-marking characterization (`usagemarking.test`: -parsedargs/-badarg/-parsestatus/-scheme marking primitives plus goodchoice highlighting of selected/default-in-effect choice words, asserted by SGR-parameter subset against the live colour arrays; the G-049 nocolour/colour-leak GAP pins flipped 2026-07-10 to scheme-statelessness assertions), the G-049 parse-status structure (`parsestatus.test`: punk::args::parse_status overall/per-argument statuses, badarg for type/allocation failures, -caller attribution, errorcode -argspecs stripping), and tclcore doc/interpreter behavioural parity (`tclcoreparity.test`, G-054, gated on have_tclcoredocs: 'string is' class choices equal the live-harvested set, per-class docids exist, error-vs-ok agreement across the probe matrix, version-note labels conditional on class presence - expectations derived from the running interpreter, green on 8.6/8.7/9.0; under 8.6 run the file directly via a plain tclkit + tcltest driver since runtests' harness needs newer infrastructure)
- `punk/args/` — punk::args tests (`testsuites/args/`): parsing, choices/choicegroups, forms, rendering/indentation characterization, synopsis display characterization (`synopsis.test`: basic italic argname/`<type>` styling, longopt `--x=` alias forms, literal/literalprefix/stringstartswith/stringendswith type-alternates rendering unitalicised, option alternate parenthesization, multi-element clause display incl `?type?` members and argname tail-word hints, `-typesynopsis` value-element lists and option passthrough incl documenter ANSI, and the small-restricted-choice-set literal rule: 1-3 restricted choices render as unitalicised `|`-joined literals in leader/option/value positions with choicegroups counted, >3 or `-choicerestricted 0` falling back to italics, `-typesynopsis` taking precedence), usage-marking characterization (`usagemarking.test`: -parsedargs/-badarg/-parsestatus/-scheme marking primitives plus goodchoice highlighting of selected/default-in-effect choice words, asserted by SGR-parameter subset against the live colour arrays; the G-049 nocolour/colour-leak GAP pins flipped 2026-07-10 to scheme-statelessness assertions), the G-049 parse-status structure (`parsestatus.test`: punk::args::parse_status overall/per-argument statuses, badarg for type/allocation failures, -caller attribution, errorcode -argspecs stripping), and tclcore doc/interpreter behavioural parity (`tclcoreparity.test`, G-054, gated on have_tclcoredocs: 'string is' class choices equal the live-harvested set, per-class docids exist, error-vs-ok agreement across the probe matrix, version-note labels conditional on class presence - expectations derived from the running interpreter, green on 8.6/8.7/9.0; under 8.6 run the file directly via a plain tclkit + tcltest driver since runtests' harness needs newer infrastructure)
- `punk/ns/` — punk::ns tests (`testsuites/ns/`): cmdwhich/cmdinfo/cmd_traverse doc-lookup flow (`cmdflow.test`, G-040 parity) and cmdhelp usage-rendering integration (`cmdhelp.test`: scheme selection, goodarg/badarg marking incl type/allocation failures, goodchoice highlighting of supplied/default choice words, alias path, cmdinfo result shape, queried-command failure attribution, and `-return dict` parse-status returns (G-049 - its GAP pins flipped 2026-07-10); remaining GAP pins for pseudo-command cmdtype + space-form docid prefixes (G-051, real `string is` pins behind the have_tclcoredocs constraint), TclOO undocumented-method fallback (G-052), and synopsis marking absence (G-050))
- `punk/mix/` — punk::mix::cli tests (prune helpers, punkcheck virtual sources), punk::mix::commandset::repo fossil move/rename characterization tests (`testsuites/repo/`, FOSSIL_HOME-isolated; GAP-marked tests pin behaviour G-022 will change), punk::mix::commandset::loadedlib tests (`testsuites/loadedlib/libsearch.test`: 'dev lib.search' match semantics via -return list — wrap-glob default, =exact prefix, case rules, explicit globs, version aggregation — plus the loadedlib 0.2.0 contract: deep discovery by default (deep .tm modules found without -refresh, registration persists), -refresh = genuine re-scan (epoch incr + rediscovery picks up .tm files added to already-scanned dirs), and highlight working without the shell-global a+ alias; shared provisioned child interp sourcing the source-tree libunknown directly — see the file's ORDERING NOTE), and the MULTISHELL polyglot build machinery (`testsuites/scriptwrap/multishell.test`: scriptset wrap via the punk.multishell.cmd template - structure/LF-only/determinism, checkfile 512-byte label validation of fresh wraps AND the committed bin/runtime.cmd, the runtime scriptset round-trip byte-identity pin, and platform-gated execution smoke: cmd.exe→powershell payload on windows, sh payload on unix or via the `wsllinux` capability constraint from `src/tests/testsupport/wslprobe.tcl` - staged to the WSL distro's native filesystem, G-059)
- `punk/lib/` — punk::lib tests (`testsuites/lib/`): range/index/parse/compat/interp_sync utilities, G-058 static-baseline seeding (`staticseed.test`: interp_sync_package_paths/snapshot_package_paths propagate a simulated ::punkboot static baseline and seed `load {} <prefix>` ifneeded mappings; no-op without a baseline), and the repl command-completeness engine (`commandcomplete.test`: punk::lib::system::incomplete pending-opener stacks - the info-complete quoting quirk progression (`set x "{*}{"` standalone vs in-proc-body), single openers, tabs, escapes, incomplete<->info-complete parity property; pre-repl-refactor characterization, see goals/G-044 detail preserve-list)

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

@ -310,6 +310,9 @@ GFLUSH line"
-result [list 0 2 4]
#--- choicelabels conventions ----------------------------------------------------------
#choicelabel fixtures use 4+ choices: a restricted choice set of 1-3 members displays as
#literal alternates in the usage synopsis line (2026-07-11), which would make markercol
#match choice-name markers there instead of in the choices cell being measured.
test rendering_choicelabel_one_space_convention {a single-line choicelabel's leading one-space indent (the 'i string is' convention) is preserved relative to its choice name}\
-setup $common -body {
@ -320,7 +323,7 @@ GFLUSH line"
-help\
"cmd help."
@values -min 1 -max 1
mode -choices {LCALPHA LCBETA} -choicelabels {
mode -choices {LCALPHA LCBETA LCPAD3 LCPAD4} -choicelabels {
LCALPHA " LALABEL text"
LCBETA " LBLABEL text"
} -help\
@ -351,7 +354,7 @@ GFLUSH line"
-help\
"cmd help."
@values -min 1 -max 1
mode -choices {MLEQUAL MLDEEP} -choicelabels {
mode -choices {MLEQUAL MLDEEP MLPAD3 MLPAD4} -choicelabels {
MLEQUAL\
" QLA_L1 label line
QLA_L2 label line"
@ -591,7 +594,7 @@ GFLUSH line"
-help\
"cmd help."
@values -min 1 -max 1
ftype -choices {arta artb} -unindentedfields {-choicelabels} -choicelabels {
ftype -choices {arta artb artc artd} -unindentedfields {-choicelabels} -choicelabels {
${$DYN_RART}
} -help\
"ftype help."

182
src/tests/modules/punk/args/testsuites/args/synopsis.test

@ -71,7 +71,10 @@ namespace eval ::testspace {
namespace delete ::testspace::testns
}\
-result [list\
"# summary\n::testspace::testns::t1 \[[a+ italic]subcmd[a+ noitalic]\]"\
{*}[
#subcmd has 2 restricted choices -> displays as unitalicised literal alternates c1|c2
]\
"# summary\n::testspace::testns::t1 \[c1|c2\]"\
"# summary\n::testspace::testns::t1 c1 [a+ italic]v1[a+ noitalic]"
]
@ -178,6 +181,183 @@ namespace eval ::testspace {
expected-error1\
{--verbose received}
]
#added 2026-07-11 (agent) - characterization of synopsis literal-vs-italic display: type-alternates,
# -typesynopsis passthrough (incl documenter ANSI), and the small-restricted-choice-set literal rule
test synopsis_typealternates_literals {literal()/literalprefix()/stringstartswith()/stringendswith() type-alternates display unitalicised in value position}\
-setup $common -body {
namespace eval testns {
punk::args::define {
@id -id ::testspace::testns::t1
@cmd -summary summary
@values -min 1 -max 2
mode -type literalprefix(text)|literalprefix(binary) -optional 0
pat -type stringstartswith(pre)|stringendswith(.txt) -optional 1
}
}
lappend result [punk::ansi::grepstr -h + -return matched -v {^##} [punk::ns::synopsis ::testspace::testns::t1]]
}\
-cleanup {
namespace delete ::testspace::testns
}\
-result [list\
"# summary\n::testspace::testns::t1 text|binary \[pre*|*.txt\]"
]
test synopsis_typealternates_option_parens {alternates on an option value are parenthesized to disambiguate from flag-alias pipes; non-matching alternative stays italic <type>}\
-setup $common -body {
namespace eval testns {
punk::args::define {
@id -id ::testspace::testns::t1
@cmd -summary summary
-o1 -type literal(auto)|int
}
}
lappend result [punk::ansi::grepstr -h + -return matched -v {^##} [punk::ns::synopsis ::testspace::testns::t1]]
}\
-cleanup {
namespace delete ::testspace::testns
}\
-result [list\
"# summary\n::testspace::testns::t1 \[-o1 (auto|[a+ italic]<int>[a+ noitalic])\]"
]
test synopsis_clause_multielement {multi-element clause type: argname tail words display per element, ?type? members wrap in brackets}\
-setup $common -body {
namespace eval testns {
punk::args::define {
@id -id ::testspace::testns::t1
@cmd -summary summary
@values -min 1 -max 1
{v1 A B} -type {string ?int?} -optional 0
}
}
lappend result [punk::ansi::grepstr -h + -return matched -v {^##} [punk::ns::synopsis ::testspace::testns::t1]]
}\
-cleanup {
namespace delete ::testspace::testns
}\
-result [list\
"# summary\n::testspace::testns::t1 [a+ italic]A[a+ noitalic] \[[a+ italic]B[a+ noitalic]\]"
]
test synopsis_typesynopsis_value_elements {-typesynopsis list on a clause value: empty members keep default display, non-empty members display verbatim unitalicised}\
-setup $common -body {
namespace eval testns {
punk::args::define {
@id -id ::testspace::testns::t1
@cmd -summary summary
@values -min 1 -max 1
{file FNAME FCOUNT} -type {string int} -typesynopsis {{} count} -optional 0
}
}
lappend result [punk::ansi::grepstr -h + -return matched -v {^##} [punk::ns::synopsis ::testspace::testns::t1]]
}\
-cleanup {
namespace delete ::testspace::testns
}\
-result [list\
"# summary\n::testspace::testns::t1 [a+ italic]FNAME[a+ noitalic] count"
]
test synopsis_typesynopsis_option_passthrough {-typesynopsis on an option passes through verbatim - including documenter-supplied ANSI styling}\
-setup $common -body {
namespace eval testns [string map [list %TS% "[a+ italic]n[a+ noitalic]"] {
punk::args::define {
@id -id ::testspace::testns::t1
@cmd -summary summary
-x -type string -typesynopsis <n>
-y -type string -typesynopsis %TS%
}
}]
lappend result [punk::ansi::grepstr -h + -return matched -v {^##} [punk::ns::synopsis ::testspace::testns::t1]]
}\
-cleanup {
namespace delete ::testspace::testns
}\
-result [list\
"# summary\n::testspace::testns::t1 \[-x <n>\] \[-y [a+ italic]n[a+ noitalic]\]"
]
test synopsis_choices_small_literal {restricted choice sets of 1-3 members display as unitalicised literal alternates in leader, option and value positions}\
-setup $common -body {
namespace eval testns {
punk::args::define {
@id -id ::testspace::testns::t1
@cmd -summary summary
@leaders -min 1 -max 1
cancel -choices {cancel}
@opts
-align -choices {left centre right}
@values -min 1 -max 1
where -optional 0 -choices {top bottom}
}
}
lappend result [punk::ansi::grepstr -h + -return matched -v {^##} [punk::ns::synopsis ::testspace::testns::t1]]
}\
-cleanup {
namespace delete ::testspace::testns
}\
-result [list\
"# summary\n::testspace::testns::t1 cancel \[-align (left|centre|right)\] top|bottom"
]
test synopsis_choicegroups_counted {choicegroups members count toward the small-choice-set literal display}\
-setup $common -body {
namespace eval testns {
punk::args::define {
@id -id ::testspace::testns::t1
@cmd -summary summary
@values -min 1 -max 1
mode -optional 0 -choicegroups {g1 {a b}}
}
}
lappend result [punk::ansi::grepstr -h + -return matched -v {^##} [punk::ns::synopsis ::testspace::testns::t1]]
}\
-cleanup {
namespace delete ::testspace::testns
}\
-result [list\
"# summary\n::testspace::testns::t1 a|b"
]
test synopsis_choices_fallback_italic {more than 3 choices, or -choicerestricted 0, fall back to italic argname/type display}\
-setup $common -body {
namespace eval testns {
punk::args::define {
@id -id ::testspace::testns::t1
@cmd -summary summary
-t -type dict -choices {a b} -choicerestricted 0
@values -min 1 -max 1
mode -optional 0 -choices {a b c d}
}
}
lappend result [punk::ansi::grepstr -h + -return matched -v {^##} [punk::ns::synopsis ::testspace::testns::t1]]
}\
-cleanup {
namespace delete ::testspace::testns
}\
-result [list\
"# summary\n::testspace::testns::t1 \[-t [a+ italic]<dict>[a+ noitalic]\] [a+ italic]mode[a+ noitalic]"
]
test synopsis_choices_typesynopsis_precedence {an explicit -typesynopsis overrides the small-choice-set literal display}\
-setup $common -body {
namespace eval testns {
punk::args::define {
@id -id ::testspace::testns::t1
@cmd -summary summary
-t -choices {x y} -typesynopsis FOO
}
}
lappend result [punk::ansi::grepstr -h + -return matched -v {^##} [punk::ns::synopsis ::testspace::testns::t1]]
}\
-cleanup {
namespace delete ::testspace::testns
}\
-result [list\
"# summary\n::testspace::testns::t1 \[-t FOO\]"
]
}
tcltest::cleanupTests ;#needed to produce test summary line.

Loading…
Cancel
Save