Browse Source

goals: G-062 achieved archival + archive-loss prevention (sweep rules, backlink markers, goals_lint)

Finalises the G-062 achieved flip staged by the prior session (punkshell
0.12.1): LICENSE.txt (BSD-2-Clause, Julian Marcel Noble 2023-2026) at the
repo root, README/punkproject.toml/AGENTS.md license declarations, index
record to GOALS-archive.md and detail file to goals/archive/.

Archive-loss prevention, from the G-015 incident (its achieved-status
verification parenthetical was silently dropped by the 2026-07-11 two-tier
restructure's Status-line normalisation; the text is restored to the G-015
archive file's Notes with provenance):

- root AGENTS.md 'Doc Restructures': deleted-line conservation check before
  committing bulk doc migrations, structured-field-normalisation hazard
  (relocate prose to body sections first), moves separated from edits
- goals/AGENTS.md: lintable header field grammar (one line per field, bare
  Status values), flip-time verification evidence recorded in the body, the
  archive move content-identical, and a new archive-time reference sweep:
  pending-tense mentions of the archived goal updated in the live tier, and
  actionable archived notes pointed to from the affected live goal's Notes
- GOALS.md maintenance rules: achieved-flip clause extended to require the
  evidence write-up and the reference sweep

Retroactive reference sweep over all 12 archived goals (25 live detail
files): pending-tense fixes (G-002 G-008 G-013 G-016 G-017 G-021 G-022
G-031 G-042 G-044 G-050 - G-017's Goal/Acceptance annotated as
gate-satisfied, contract meaning unchanged), forward pointers pushed from
archived notes (G-062 license-seeding follow-on -> G-012/G-027, G-037
vendorlib/vfs drift semantics -> G-024, G-058 static-baseline mechanism ->
G-005), and one 'Archived-goal references' Notes bullet per remaining
referencing file so every live mention of an archived goal is resolvable
without consulting the archive index.

scriptlib/developer/goals_lint.tcl (new, plain tclsh): validates detail-file
header grammar, strict status values in both tiers, orphan rules in both
directions, Status/Scope mirror consistency, plus an informational (never
fatal) warning for live files referencing an archived goal with no
achieved/archive marker - baseline now zero warnings. Root AGENTS.md
scriptlib/developer ownership note widened to cover developer utilities.

Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
master
Julian Noble 3 days ago
parent
commit
99af43c8be
  1. 12
      AGENTS.md
  2. 4
      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. 2
      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

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`.

4
CHANGELOG.md

@ -5,6 +5,10 @@ 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`).

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)

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

@ -49,7 +49,7 @@ 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 -

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.12.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
Loading…
Cancel
Save