Skip to content

grund 0.13.0

Choose a tag to compare

@github-actions github-actions released this 05 Sep 20:14
· 2197 commits to main since this release

Added

  • §FS-workspace.2.2, §FS-check.4.9: a workspace member can be declared absent, and the run says which namespace it did not check. A checkout where one member is legitimately missing — a private submodule CI never fetches, a sparse checkout, a sibling repository vendored in only for release builds — had no grund check run at all. Leaving the entry in [workspace] members is a fatal config error, and taking it out unregisters the alias, so every citation into that namespace becomes an unknown-alias error at its own site (§FS-check.3.8) — thousands of lines in a tree that cites the namespace widely. One path refuses to start and the other calls the whole tree broken, and neither is a run. [workspace] optional_members is the third path: a sibling of members with the same grammar, whose entries are the members this repository has declared may be absent. Present, an entry there is an ordinary member and is checked to the letter. Absent, the block loads without it, the run continues, and the namespace it would have contributed is unverified — a third state beside resolved and unknown (§FS-workspace.4), reported once per namespace at the optional_members line rather than once per citation site. This is an opt-out, not a softer default: a member listed in members and missing still fails at the same line with the same verdict, and the message now names the key that would have made the absence legal. Absent means the path is not a directory and nothing more, so the empty directory git leaves for an uninitialized submodule is present — widening the test would put a typo'd directory on the unverified path and move the blind spot's edge somewhere no line of the repository records (§FS-workspace.2.2.1). The alias of an optional member is the entry's last path segment in both checkouts, and a present member whose project_name disagrees with it is a config error naming both names (§FS-workspace.2.2.2): otherwise one citation text would resolve in the tree that has the member and quietly name nothing in the tree that does not, which is a trap only the checkout least equipped to notice can spring. A trailing /* glob may not be optional, because an absent parent names no namespaces — the refusal says to list one concrete entry per namespace instead. An absent entry's alias is unique among its siblings like every other alias (§FS-workspace.3): an entry that collides with a present member, with the root project, or with another optional entry is a launch-time error, because a run that announced a namespace as unchecked while checking a different project under that same name would contradict itself in two adjacent lines. An entry that appears in both members and optional_members is refused from the config text rather than from the checkout, so the same pair of lists is refused in the tree that has the directory and the tree that does not. One entry written twice is one member in both checkouts as well, folded the way a repeat in members is folded rather than refused (§FS-workspace.6.1): a repeat names one directory and one namespace, so there is nothing to discard by folding it, and refusing it only where the directory is missing would be the checkout-dependent verdict the key exists to remove. A block whose every member is an absent optional one puts no project in scope, which is not an error either — the run announces each absent member and earns the empty-scan caution beside them, in the wording that is true of it: nothing was looked for under [scan] include, so the caution says what happened rather than offering a grund init scaffold for a tree that is already there (§FS-check.2.2). What the run pays is a blind spot, and it is a declared one: §REQ-no-missed-citation.2 enrols it as the fourth, chosen by the repository one written entry at a time and announced by every run that takes it. The announcement is a located warning on stdout, unlike its two neighbours (§FS-check.4.7, §FS-check.4.8), which are CLI-level lines on stderr: nothing here is misconfigured and no other command's output is wrong, and this is the one run that is deliberately incomplete and still exits 0 — so the exit code carries nothing about coverage and the report has to. It withholds the success marker like any other warning (§FS-check.2.1), names no remedy, and names no release, because the skip is the standing price of the opt-out rather than a state to migrate off. The key works in a nested [workspace] block exactly as it does at the outermost root, and grund config show prints it between members and include_root (§FS-config.3.8). It is additive with an empty default, so grund_config_version stays at 1 and no configuration has to be edited. Decided in §DF-optional-workspace-members. Closes issue #85. (PR #195)

  • §FS-check.4.10, §FS-workspace.6.1: a [workspace] block that opted out of being a project no longer takes its own files out of every scan in silence. include_root = false says the block's root is not a project, and the enclosing scan stops at the member boundary (§FS-workspace.6), so that directory's own files are read by nobody — a declaration there reaches no catalog, a citation there is never checked, and --full widens a project's scope and has no project here to widen. §FS-workspace.6.1 has said exactly that in prose since the key existed; no run said it, and over a grouping directory holding two dangling citations check, check --full and list were all silent and all exited 0. Every command that resolves a block's member boundary — check, list, refs, cover, fmt and the rest — now prints one CLI-level warning: on stderr naming the block's own include_root line, the tree no project scans, what that costs, and the two remedies: set include_root = true, or point another project's [scan] include at the directory. It fires only where the block's own tree actually holds a file a scan would have read — the block's default scope, less the roots that are not on disk, the roots its own members already cover, and the roots another project of the run owns, then probed with the scanner's own walk and stopped at the first hit — so a grouping directory that only groups, a tree of unscanned file types, an excluded, gitignored or hidden tree, and a scope root that is a symlink into a sibling project all stay silent, and the walk is paid only by a block that opted out. That last one is §FS-workspace.6's boundary in the one direction a member list cannot see, and it is asked of the root itself as well as of the descent, because a walk root is never pruned. A block with no member in scope is not this finding either: that is already a config error at its own line, and a run whose workspace expansion fails carries no such line for the blocks below its root, because the answer depends on where that run's other projects are and a failed expansion never produced that list — a declared, bounded blind spot that clears on the next run. It is once per block per run, keeps its text under --format json with no diagnostic code, and never moves the exit code — but like every warning it stands in place of the success marker (§FS-check.2.1), which this finding is the first [workspace] caution to have to say out loud, because it is the first that can fire on an otherwise clean run. Unlike its two siblings §FS-check.4.7 and §FS-check.4.8 it names no release it becomes an error in, and never will: §DF-unread-opted-out-block.2.3 states the rule for all three — a finding is eligible to become an error only when every repository in that state is wrong and the configuration has a way to say "I meant it" — and a grouping directory holding a README nobody needs checked fails both, which leaves §REQ-backwards-compatibility.2's deprecation path with nothing to schedule. This is the last of the two silent-scope holes §DF-absorbed-scan-warning.4 left open. Library callers: CheckOutput gains a public unread_opted_out_blocks: usize field carrying how many such lines the run printed before the report existed, an additive break for exhaustive struct literals and exhaustive destructuring; the struct has no Default, so unlike the InitOpts entry below there is no ..Default::default() spelling that absorbs it. Decided in §DF-unread-opted-out-block. Closes issue #71. (PR #196)

  • §FS-fmt.7, §REQ-no-wrong-citation.3: what grund fmt is is written down once, and every clause of it now has a check. Twelve fmt defects were each fixed as its own bug, and the fix commits kept independently rediscovering fragments of one sentence: fmt is the model grund check verifies plus a write step, never a second implementation of it. §FS-fmt.7 states that as four rules, each citable and each with an artifact that fails when it breaks — scope-equivalence (§FS-fmt.7.1), reader-equivalence (§FS-fmt.7.2), preview-equivalence (§FS-fmt.7.3), and no write without a complete model (§FS-fmt.7.4). Reader-equivalence is equality of the (path, reason) pairs rather than of the bytes of the line, with nothing was rewritten: named as the one licensed difference: §FS-fmt.3 spells a strict abort's line differently on purpose, because the two exit 2s mean opposite things, so byte equality would have put §7 in contradiction with §3 on the day it landed. The first three rules are behavioral and are pinned as cross-run properties over a corpus of six tree shapes — clean, strict-abort, partial source-only, two-scope, workspace, and workspace-member-abort — rather than over the one shape whose defect prompted each; the new fmt-partial-scan-error-still-reports e2e case pins the partial form of §FS-fmt.7.2 that no golden covered. The fourth is structural, because its prose form had already failed twice: completeness of the declaration set is now a precondition of the consumer instead of a habit of each producer, so the whole-project findings a fmt walk resolves shorthands and cross-references against are accepted only as a CompleteFindings proof, obtainable from a scan that met no error or from a reuse that checks the same field a fresh scan would have refused on. Handing that consumer a bare findings collection — the mistake issue #105 actually was — is now a program that does not build, and scripts/check_fmt_complete_findings.sh is the check that it stays one. The rule has a direction, and §7 says so: equivalence binds fmt to check's model, and never check's findings to what fmt is able to rewrite — the second coupling is what §FS-fmt.2.4 has and what makes a citation inside a string literal unflaggable by any command (issue #83). No behavior changes anywhere: no output, exit code, or diagnostic text moves, and every existing golden passes unmodified. Decided in §DF-fmt-one-model. Closes issue #106. (PR #192)

  • §FS-check.4.8, §FS-workspace.6.1: a [workspace] block no enclosing block lists is no longer silently ignored. Such a block is claimed by nobody: at the outer scope it is ignored and its whole subtree is absorbed into the enclosing project's namespace, while a run started at it names every project from itself — so the two scopes spell the same projects differently and a citation passes the inner check and fails the run CI does. Every command whose run walks the tree — check, list, refs, cover, fmt, and the ID read — now emits one CLI-level warning: on stderr naming the block's [workspace] line, saying its projects are absorbed rather than named under their own alias path, and giving the two config edits that clear it: list the directory in the enclosing [workspace] members, or keep it out of that project's [scan]. Only the outermost unlisted block of a chain is reported, because one edit fixes the chain; include_root = false on the block changes nothing, because it still contributes a segment to every alias path below it. The scope is the walk the run already makes, at one config probe per walked directory — which is what finds the .agents/grund.toml form the walk never descends into — so a block behind [scan] exclude, an ignore file, a member boundary, or a narrowed scope stays unreported, and both §FS-workspace.6.1 and §DF-nested-workspaces.3.4 still record that residue. It is a warning, so the exit code is unchanged — but like every warning it stands in place of the success marker, and the message names the release it becomes an error in (§RM-unlisted-workspace-error), the deprecation path §REQ-backwards-compatibility.2 asks of a finding no grund command can write the fix for. Decided in §DF-unlisted-workspace-block. Closes issue #72. (PR #191)

  • §FS-init.1, §FS-init.4, §AR-ci.3: grund init --check turns the --dry-run preview into a gate. A managed agent-entrypoint block can go stale in its rendered text while its (vN) heading is still current, and until now nothing in the toolchain could fail a build over it: grund check verifies the block's version rather than its bytes (§FS-check.3.5), and grund init --dry-run saw the drift, printed would-update AGENTS.md, and exited 0. --check runs that same preview, prints that same report byte for byte, writes nothing under any flag combination including --force, and exits 1 when any reported path is a would-write , would-append , or would-update ; a note: and the next: block never earn it, and a refused target still exits 2. --dry-run itself deliberately keeps its 0: 1 is the findings slot §FS-cli.5 freezes and init had never used, so --check fills a hole in the mapping, whereas moving --dry-run would change the verdict an existing green CI step gets on upgrade — the one thing §REQ-backwards-compatibility.1 puts the exit-code mapping out of reach of. This repository now runs the gate on itself, as a grund-init-check pre-commit hook placed after grund fmt --write so the block is measured once fmt has had its say. Library callers: InitOpts gains a public check: bool field, an additive break for exhaustive struct literals in the same way --no-vcs was in 0.10.1; ..InitOpts::default() is unaffected. Closes issue #172. (PR #185)

  • §FS-fmt.2.5, §FS-config.3.10: grund fmt can be suppressed per file and per region, without taking the citations out of check. A new [fmt] exclude list names files the command performs no rewrite in, as gitignore-style globs against the config root; a grund:fmt off comment line — <!-- grund:fmt off --> in Markdown, a [scan] comment_prefixes comment in a source file — suppresses the rewrite from the next line until the matching on or the end of the file. Both cover all four rewrite classes (§FS-fmt.2.5.1, §FS-fmt.2.5.2), not just cross-reference wrapping, because every one of them disturbs the thing the scopes exist to protect: a hand-aligned diagram in an HTML <pre> block or an indented block, whose edge labels are citations that must stay live for grund refs and the LSP. Nothing else changes — a suppressed file is still walked and still checked, so grund check reports exactly what it reported before, and the dry run and --write agree about the scope by saying nothing about it. The LSP's live on-type expansion honours both scopes as well (§FS-lsp.1.4), so an editor cannot canonicalize a shorthand on a line grund fmt --write refuses to touch. A kind's index entries outrank both scopes and are still linkified (§FS-fmt.2.5.3), the same precedence §DF-index-always-linkified already has over [fmt.cross_refs] enabled = false, so no suppression can leave an index in a state §FS-check.3.17 reports and fmt refuses to repair. The key is additive with an empty default, so grund_config_version stays at 1 and no configuration has to be edited. Decided in §DF-fmt-suppression. Closes issue #154. (PR #184)

  • §FS-config.3.4.8, §FS-check.3.6: grounding is now asked per place and per level. require_grounding and a new grounding_level are [[kinds]] row keys, each with its [reference] twin as the default for rows that do not say — the shape index already has (§FS-config.3.4.2), a global default with the row winning. So "every file under skills/ must cite a declared ID" is finally sayable without also demanding it of every workflow and build script in the scan, which is why that hole stayed open and §FS-check.2.2.1 could only warn about it — and that warning now names the row's key rather than the global one. grounding_level is an integer in Markdown heading levels: 1 is the file, which is the unit every configuration had before the key existed, 2 adds every ## subtree, 6 every heading; a leaf cites directly, a parent is satisfied by any descendant, and a file with no heading at the level is one unit, so nothing passes vacuously. A source file has no headings, so it gets the two ranks grund can see without parsing code (§FS-non-goals.3) — unindented doc-comment blocks at level 2, all of them above it — read by indentation, not by syntax. [citations] obligations follow the same unit (§FS-check.3.11), so whether a place's files must cite and what they must cite are asked of one thing. grund check --require-grounding and the global key stay one knob: the flag sets the same default, and an explicit false on a row still wins (§FS-check.1). Both keys are additive, so grund_config_version stays at 1 and no configuration has to be edited to keep the meaning it had. One finding is added to trees that changed nothing: an inline declaration in a non-citable home no longer grounds the file it sits in (§FS-check.3.6.2), because a declaration there is misplaced to begin with (§FS-check.3.7). Every file it reaches already carries a must not be declared in <home> error at the same line, so no passing tree starts failing and no verdict moves (§REQ-backwards-compatibility.1) — but a run's bytes do, which is why it is written here. Decided in §DF-require-grounding.4. Closes issue #150. (PR #182)

Changed

  • §FS-config.3.4.6, §FS-distribution.4.2: the [[kinds]] prefix key stops loading, at the release its warning named. prefix was this key's name until citable = false made it wrong for half the table; 0.12.0 renamed it to kind, kept the old spelling loading, and warned every config that still used it, naming 0.13.0 as the release it would stop working in — the deprecation path §REQ-backwards-compatibility.2 asks of a renamed config key. This is that release, so this is a breaking change for any configuration still on the old spelling, which is what the window was for. A [[kinds]] entry that spells prefix is now refused rather than read with the key ignored — an ignored name would leave the row with no kind and change what the configuration means without saying so — so every walking command exits 2 on it with an ordinary config error (§FS-config.4.3) anchored at the line prefix is written on: error: grund.toml:4: [[kinds]] `prefix` was removed in grund 0.13.0 — rename it to `kind` . An entry that sets both kind and prefix earns that same error at that same line, and the old "sets both" error is gone with the key: with one of the two names removed there is nothing left to disambiguate. The migration is the rename, by hand, and that is why the error names the line to make it on — a grund before 0.13.0 did it in one grund config show > grund.toml, but from 0.13.0 the config does not load, so nothing that has to read it can help. grund_config_version stays at 1: every affected config is already version 1 or carries no version key, so no version gate could catch one and only this rejection can. The deprecation warning, its deprecated-config-key diagnostic and the release constant behind it are deleted, and the roadmap milestone that scheduled the removal is retired into §FS-config.3.4.6. Retiring that milestone took away the last thing tying the 0.13.0 in the error to the version actually published, and the direction it left open is the dangerous one: a bump that reaches a deadline reddens a ramp test, while a bump that stays below a release a landed change already names reddens nothing, so a scheduled patch release would have published this breaking change as 0.12.4 under an error saying 0.13.0. The release path now asks the question directly rather than through a test no publication path runs: scripts/check_release_ramps.py reads the release each message names out of the tree's own message text — the sources under crates/ and the e2e goldens that pin the bytes a user sees — and refuses a version that contradicts one, in both directions and for every ramp rather than this one (§FS-distribution.4.2). release.yml's verify job runs it on the version it is about to publish, which is every publication path including the dry run both bump helpers wait on, and auto-bump.yml and release-minor.yml run it again before they push a candidate branch. On this tree it reports an empty window — at or above 0.13.0 for the removal, below 0.13.0 for the missing-index-entry warning that still promises it — which is the machine-readable form of what this ticket said in prose: the sibling ramp has to land before anything is cut. Decided in §DF-non-citable-kinds.2.4. Closes issue #201. (PR #202)

  • §FS-check.3.18, §REQ-backwards-compatibility.2: a declaration its kind's index does not list is now an error, in the release the warning named. Since 0.12.0 the finding has carried its own deadline in its message text — an index entry becomes an error in grund 0.13.0 — and this is that release, so the ramp ends here rather than slipping: a warning that names a release it never reaches has told every user a deadline and then let it pass. What moves is the verdict and nothing else. The run exits 1 instead of 0, the finding is on the error path and no longer stands in for the success marker as a warning, and under --format json the diagnostic's severity reads error — while its code stays missing-index-entry, because a code is the stable identifier a consumer greps for and it does not move with a severity (§FS-errors.5). The finding is still anchored at the declaration's own heading rather than at the index, a folder with no index file at all still reports once per declaration in it, and the three parentheses that say how the index failed to read — (the index file does not exist), (the index file is a directory), (the index file could not be read) — are unchanged. The message trades one clause for another: the deadline goes, because a reader can no longer be told it in time to act on it, and — became an error in grund 0.13.0 reports in its place — the past tense the sibling [[kinds]] prefix removal writes, and the form the release gate reads (§FS-distribution.4.2). The fix is unchanged too, and is a human edit rather than a command: write one full Markdown link per unlisted ID into the kind's index. That no grund command writes an index entry is exactly why this half rode §REQ-backwards-compatibility.2's deprecation path instead of §REQ-backwards-compatibility.3's same-release flip, which the linked-entry half §FS-check.3.17 took on arrival — and for one release that left the two halves of one contract disagreeing about severity. They agree again. A repository that upgrades into this release and was already green stays green; one that was carrying the warning sees the lines it has been seeing since 0.12.0, with the deadline replaced by the release it landed in, on a run that now fails. The rule also moves section, because a rule that errors cannot sit under a heading called Warnings: <§>FS-check.4.6 is now §FS-check.3.18, at the end of Errors detected and directly after §FS-check.3.17, the other half of the same entry contract — and <§>FS-check.4.7 … <§>FS-check.4.12 renumber down to §FS-check.4.6 … §FS-check.4.11. Every citation in this repository moved with them and no promise to anyone outside it did: §REQ-backwards-compatibility.1 covers the CLI surface, the exit-code mapping, the JSON schemas, the config schema, the citation grammar and the managed block, and not the numbering of a section in this repository's own specification. Naming the release is also what puts this ramp inside the gate the entry above landed: a promise about a release breaks by arriving early as surely as by slipping, and scripts/check_release_ramps.py reads the release each message names out of the tree's own message text and refuses a version that contradicts it. So a 0.12.4 carrying this error — one minor before every run of 0.12.0 said it would arrive — is now refused on every publication path, rather than by a test on a candidate branch no test suite runs on. And it is what opens the window the entry above reports empty. That bullet was written against a tree where the prefix removal already required at or above 0.13.0 while this warning still promised 0.13.0 and so held the tree below it — two bounds that cross, which is why the script there can cut no version at all. This flip spends the promise, so the release both ramps named is now one this tree can be cut as: no message in this tree contradicts a release of 0.13.0, and a 0.12.4 is answered with this tree can be cut as at or above 0.13.0 and below 0.14.0 — the upper bound being the two workspace ramps (§FS-check.4.7, §FS-check.4.8) that name 0.14.0 and are still ahead. Decided in §DF-index-compatibility-ramp. Closes issue #200. (PR #203)

  • §AR-ci.9 / §AR-ci.1: the fissile config moves to .agent-grounds/fissile.toml, and the CI pin moves to 0.8.3 in the same change. .agents/ is where agent instructions live, and a managed agent sandbox mounts that directory read-only — so a file-size budget kept there is one the agent working in the checkout cannot repair. fissile 0.8.3 is the first release whose config discovery looks in .agent-grounds/; the old path stays a deprecated fallback that still works. The move and the pin are one commit because either alone is a regression: under a pin below 0.8.3 the relocated file is not found at all, and fissile check then falls back to its built-in defaults and reports success at exit 0, which is an unenforced gate that looks green. Nothing else moves — the file's bytes, every rule, message, and both exception registries are what they were at the old path. tests/integration/test_architecture_page_paths.py learns the new home in the same change, so an architecture page naming a file under .agent-grounds/ is held to existing on disk exactly as one naming .agents/ already was. (PR #197)

  • §FS-distribution.4.1: after a release, main opens the next patch as X.Y.Z-dev instead of keeping the version it just published. A build from main previously reported the tag it was already ahead of, so a merged-but-uninstalled fix was indistinguishable from an installed one. (PR #179)

  • §FS-check.2.2.1: grund check now warns on stderr when a walked folder with real non-entry content has an explicit citation-direction obligation but no unit to evaluate, with citable and non-citable guidance and no exit-status change. (PR #176)

  • §FS-config.3.9, §FS-init.5: add one canonical citation-directions explanation and checked copies/renders across setup surfaces; link it from the README and static entrypoint guidance. Closes issue #148. (PR #177)

  • §AR-ci.9: the docs file-size hard ceiling moves from 1500 to 2000 lines; the soft budget stays at 750. The four foundation projects now state one shared documentation budget, and its principle is the one this repository already wrote: a citable spec is reached through a citation and read as a lead, a section, or a table of contents (§FS-show), so its length is not charged to every read the way a source file's is, while an entrypoint — README.md, AGENTS.md, CLAUDE.md, a SKILL.md — is read whole into every session and keeps the tight 250/500, and a changelog is an append-only record that is not line-measured. Only the ceiling moves, because only the ceiling has to hold for four repositories at once: the hard tier has no override, so it has to sit far enough above the natural size of the documents it measures that no ordinary edit reaches it, and rhei's largest spec measures 1482 lines — 18 lines under 1500, which is one edit. 2000 is not a new number either; it is the ceiling rhei's own AR-source-file-size.1 already declares for any file. Nothing in this repository is affected in either direction: the soft tier fires on nothing today (the largest document, §FS-workspace, measures 530 lines) and no exception in docs/file-size-agent-exceptions.toml or docs/file-size-human-exceptions.toml targets the docs rule, so raising the limit strands none of them. Closes issue #174. (PR #175)

  • Schema: the AGENTS.md managed block moves to v8 (§FS-init.2.3.5, §DF-directions-render): the generated ### Citation directions section is re-rendered so it is exact. Every bullet now names the unit its rules are checked per — Each **DA** declaration, Each file in **skills/**, Each source file outside the Project map (**code**) that cites anything — a conjunction of alternatives is parenthesised so must = ["FS|GOAL", "AR"] reads (FS or GOAL) and AR rather than the opposite grouping, three or more alternatives take the Oxford comma, */AR is said as AR in any project instead of leaking rule grammar, a closed per-kind default folds into may cite only …, a legend says which levels gate and which suggest, and the closing line reports the global default instead of pointing back at grund.toml. With [reference] require_grounding on the paragraph also carries the grounding sentence the block never rendered, under [citations] or without it. Migration: run grund init — until then grund check reports one agents-init finding (§FS-check.3.5). Closes issue #151. (PR #181)

  • §AR-ci.9: this repository's rhei template follows the fissile config out of .agents/. .agents/rhei/templates/parallel-spec-review/ moves to .agent-grounds/rhei/templates/parallel-spec-review/, the home rhei resolves first from vjovanov/rhei#184 on, keeping .agents/rhei/ as a deprecated fallback it warns about. The reason is the one that moved fissile.toml in the bullet above: .agents/ holds agent instructions and an agent runtime may mount it read-only inside a checkout, so a template an agent has to edit cannot live behind that protection. Nothing in the repository referred to the old path, and no grund behavior changes — the directory is outside [scan] include at either name. A rhei older than that change does not look under the new home, so instantiating this template needs a rhei built from main or later. (PR #199)

Deprecated

  • §FS-config.1.2, §FS-check.4.11: a repository whose config lives at .agents/grund.toml is now told to move it to the repository root. The root is where a grund.toml belongs, and the reason is visibility (§DF-config-file-location.2.4): a dot-directory is hidden from ls, from editor file trees and from shell globs, so under the old form the question is this a grund project? has no answer a reader can see. That recommendation lived only in the specification, which is a place no repository hears it from — so grund check, grund config validate and grund config show now print one CLI-level warning: on stderr naming the file the run read and the bare grund.toml beside it that should hold it, both rendered relative to the config root so the line reads as the git mv a reader can type. It is §FS-check.4.3's finding in everything but its trigger and inherits that section's argument whole: this is a fact about which file the run read, so those three surfaces carry it and list, refs, cover, fmt and the bare ID read stay silent, because a fact that never changes between runs and is printed by every command is permanent output tools learn to filter. It is once per config rather than once per scope, so a workspace names the root's config and every member's at the path that project's config was loaded under, a member on the bare form earns nothing, and a directory carrying both names earns §FS-check.4.3's line and not this one — there the bare file won the tie, the config in force is on the home path already, and emitting both would name one move twice while disagreeing about which of the two files is the problem. Under --format json it keeps its text with the code deprecated-config-location and null path/line/sites. The exit code never moves, but like every warning the line stands in place of the success marker (§FS-check.2.1), which is a verdict change §REQ-backwards-compatibility.1 permits and this repository pays in its own e2e corpus: the fixtures that had no reason to be on the old path are now on the bare form, and the ones that remain are the ones whose subject is discovery itself. Deprecated means the tool asks you to move, not that it is going to stop reading. There is no release in which .agents/grund.toml stops being a config location, so the message names none — a deliberate departure from §REQ-backwards-compatibility.2's ramp, argued in §FS-config.1.2: a named release buys time before something breaks and is owed only where something will break, and every repository grounded before dual discovery is on the old path and is a correct configuration rather than a broken one. What the warning is for needs no deadline — it stops a new project landing on the old path by copying an old one, at the moment the sibling tools are moving their own agent-facing files out of .agents/. grund takes no part in that move and adds no .agent-grounds/grund.toml location: its config left the agent directory already, and for the stronger reason. The check and config help pages say it too, in place of the two locations named as equals. Decided in §DF-config-file-location.2.5. Closes issue #189. (PR #198)

Fixed

  • §FS-workspace.8.1.1, §FS-show.3: an ID that only a workspace member declares is named instead of refused blind. At a workspace root, grund SPEC-007-shipping for an ID the vendored member holds reported a bare ID not found: and pointed at grund list — the catalogue that, in the very same invocation, could already say which project has it. The refusal now carries the qualified spelling: ID not found: SPEC-007-shipping; did you mean vendored/SPEC-007-shipping?. The ID is not resolved into the member and grund <ID> still exits 1 with empty stdout: §FS-workspace.4 is what grund check enforces at a citation site, so a query command that printed the body would teach the reader a citation CI rejects, and one resolver across check and the query commands is the direction §DF-subproject-namespaces.3.7 states. Naming the qualified form gets the reader the same fact in one step and the spelling that resolves. Several projects declaring one ID are listed rather than chosen — sorted, joined a, b or c, cut at three, the shape §FS-check.3.8 already uses for an unknown project alias — because two declarations in two namespaces are not an ambiguity (§FS-workspace.8.1) and picking one would be a guess (§REQ-no-wrong-citation.1). Each candidate project is asked in its own [id] grammar, so a mixed-format workspace answers where carrying the current project's parse across the boundary would find nothing; the section of a <ID>.<s> lookup is not carried into the candidate, because this run never looked for that coordinate there. A narrowed run — member-local, standalone, or a <path> that resolves inside a member — holds one project and offers nothing, unchanged from today, and the grund list hint prints exactly as before wherever no candidate is named. Where one is, it gives way: it sends the reader to the catalogue this line has already searched, and its other half proposes minting an ID that demonstrably exists. --format json still carries "code":"not-found", with the candidate inside message. Closes issue #86. (PR #194)

  • §FS-fmt.2.3, §AR-scanner.1, §FS-config.3.5, §FS-check.1.3, §FS-check.2.2: two skips the scanner has always made are now stated where a reader looks for them, and the caution that misattributed one of them names the rule that actually decided. §REQ-no-missed-citation.2 asks every region the scanner passes over to be declared, and an audit against it found two that were not. The first is the fence carve-out. §FS-fmt.6.4 excused itself with "the same skip used by §2.3", and tests/e2e/cases/fmt-skips-declarations-and-fences cited that section as the one it proves — but the never-rewritten list of the only command that edits files in place named declaration headings, string literals, inline code spans, link destinations, out-of-scan files, symlinks, and suppressed scopes, and not fenced code blocks. The skip was real and already pinned; it is also the most consequential thing fmt leaves alone, since it is what makes every citation illustrated in this repository's own docs safe to write, and no section said so. The list now names it, for all four passes that rewrite, and the case's spec.refs moves off the stale FS-005-fmt.2.3 spelling onto the section that carries the claim. The second is the hidden file: the walk drops any file whose own name begins with . before [scan] extensions is consulted, while every section that wrote the walk's skips down spoke of hidden directories, so a docs/.notes.md carrying a dangling citation was invisible to grund check and to grund check --full alike and no reader could have known. §AR-scanner.1 and §FS-config.3.5 now state it as what it is — a rule about a file's own name rather than about a descent, asked per entry as the file list is assembled. That distinction is load-bearing rather than pedantic, because it makes this the one skip that reaches a walk root: a hidden include entry and a [[kinds]] file home with a hidden name are read by nobody, which is the exception §FS-check.1.3's "a gitignored, excluded, or hidden include root is read by the plain run" needed in order to stay true — and --full remains purely additive through it, because such a file is missing from both root lists rather than from one. Writing the skip down forced the third change. grund check docs/.notes.md used to answer no files under … matched grund's extensions (md, rs, go, …) about a file whose extension is md: it named the one rule that had not skipped the file and sent the reader to edit config that was never the cause, which is the misdirection the requirement above is about — a blind spot a reader is pointed away from cannot be planned around. Handed a file whose own name is hidden and whose extension is listed, the caution now names the hidden-name rule and gives the remedy (§FS-check.2.2); a hidden file whose extension is also unlisted keeps the extension message, because there the list is a true reason and naming one of two causes would be its own misdirection. The empty-scan diagnostic code, the stderr stream, and the exit code 0 do not move. The skips are declared, not removed — reading hidden files would be a behaviour change no release could take back, and the audit asked for the rules to be written down, not changed. Four e2e cases hold that line, three of them pinning that a dangling citation in a hidden file still yields success and exit 0 from check, from --full, and when [scan] include names the file itself: the executable answer a declared blind spot owes. The audit's other two items — the comment prefixes no default extension could reach, and the thin per-language dangling proof — were closed by PR #124 and are untouched here. Closes issue #98. (PR #193)

  • §FS-workspace.6, §REQ-no-wrong-citation.3: a grund fmt run narrowed below the workspace root no longer writes through a symlink into another project's files. The member boundary belongs to the directory rather than to the path that reaches it, so a link from the root project's scanned tree into a member is not descended into — but a narrowed run walked with a copy of the root config taken before member expansion, so that copy carried no member roots and the walk had nothing to stop at. grund fmt --write docs then rewrote the member's own file and spelled the cross-reference it wrote there from the root project's namespace, which is a wrong write in a tree the run does not own. The copy is now taken after expansion — the same object the workspace check has always been handed — so the narrowed walk stops at the boundary like every other walk. The change came with optional_members above, where the report has to be rendered from what the walk learned, and no test in the corpus had a symlink crossing into a member, so it landed unnamed; the new workspace-fmt-narrowed-boundary-symlink e2e case pins it, together with the other half of the rule — a link into a directory no project owns is not a boundary and is still walked and still rewritten. One residue is left as it was: grund check narrowed to the same scope still reads the linked file and reports its citation, so the two commands disagree about that file's scope. That difference predates this change and is not reconciled here. (PR #195)

  • §FS-workspace.2.1, §FS-check.4.7: a [workspace] member that swallows the block's own scan is no longer silent. members = ["docs"] beside include = ["docs"] leaves the listing block's project with nothing to read — every one of its walk roots lies under a member boundary, so its declarations reach no catalog and its dangling citations pass (§GOAL-no-dangling-refs). grund list printed nothing and exited 0; only grund check said anything, and what it said was the generic empty-scan caution naming [scan] include — the key that was already correct. Now every command that resolves a block's member boundary carries one CLI-level warning: on stderr — check, list, refs, cover, fmt, and the commands that resolve a boundary without walking it, config validate and init — at that block's members line, once per block per run and ahead of any finding, naming the member entry as the config wrote it and each covered scan root by its path under the block root. Roots and members are compared canonically, the way the walk's own prune compares them, so a member reached through a glob or a symlink covers what it actually lands on, and a nested block is reported at its own members line. One run spells the whole tree from one place, the directory it was launched in: grund init inside a member of a three-deep absorbed tree names the block above it, the block it is on, and the block below it as ../grund.toml, grund.toml and sub/grund.toml, so every path it prints resolves from where the reader is standing (§FS-errors.4). Four neighbouring shapes stay silent on purpose: a partly covered scope, which §FS-workspace.6 already specifies as the boundary working as designed; include_root = false, where the block is not a project and has no scan to lose; --full, which adds the config root as a walk root while the boundary still prunes; and a tree where no walk root exists on disk at all. A block with no [scan] include key is not one of them: the key carries a materialized default (§FS-config.3.5), so such a block has those roots rather than none and the rule is asked of them as it is of any other block's. The empty-scan caution keeps its text and still fires where it fired before — the new warning stands beside it, first. The severity is a warning with a named release rather than an error because no grund command repairs it: the fix is a judgement between repointing [scan] include and declaring the block no project, so §REQ-backwards-compatibility.3's single-release licence does not apply and §REQ-backwards-compatibility.2's deprecation path does — so the message itself names 0.14.0 as the release the finding becomes an error in, §RM-workspace-absorbed-scan-error is the milestone that flips it, and a unit test fails the version bump that reaches the deadline so the window cannot expire unnoticed. Decided in §DF-absorbed-scan-warning. Closes issue #78. (PR #190)

  • §FS-init.2.3.4.15: generated ### Workspace members lists omit the canonical project whose own entrypoint is being rendered, instead of linking that project back to its own AGENTS.md. The cross-project sentence and every foreign row keep their existing alias order, configured marker, description, relative-link, and not-yet-initialized grammar; include_root = false keeps excluding the root from member entrypoints. Closes issue #155. (PR #188)

  • §AR-workspace.9.1: refreshing the e2e goldens over an unchanged tree now rewrites nothing. UPDATE_EXPECTED=1 used to rewrite 21 tracked goldens whatever case the change was about, so a golden update arrived with churn that had to be reverted by hand or was committed by accident. The harness's reader accepted two spellings of one expectation where its writer emitted only one: empty output read back the same at zero bytes as at a lone \n, an exit golden the same terminated or not, and the reader folded \r\n to \n while the writer passed the run's own line endings through. Each surface now has exactly one on-disk form — an output golden is LF-only and empty output is a single \n, an exit golden is the decimal code and exactly one \n — write_expected folds CRLF before writing, and the 21 goldens checked in the other way are normalized once. The form chosen is the majority one already in the tree, so 21 files move rather than the 469 the issue's illustrative "write empty output as 0 bytes" would have moved; it is also the form text eol=lf and every end-with-a-newline convention expect, and it keeps \ No newline at end of file out of a diff a reviewer reads as bytes. goldens_are_in_canonical_form walks every case under tests/e2e/cases/ and examples/ and names every departure in one failure, so a hand-written golden in the old spelling is reported as itself rather than as an unexplained mismatch in an unrelated case. Closes issue #173. (PR #186)

  • §REQ-shipped-surfaces, §FS-init.5, §FS-integrations.4: nothing grund ships or prints names an ID of grund's own repository. The grund.toml that grund init writes opened by citing FS-config.3.9; the skill grund agent-setup-instructions prints byte-for-byte carried a dozen citations behind ../../docs/… paths; the payloads grund integrations --write installs into a user's dotfiles carried three dozen more, permanently, since --write is idempotent; and two printed strings — grund help config and the homeless-kind config error — ended in one each. None of those resolve in the repository they land in, and a project with an FS-config of its own resolves them to an unrelated document; because they are plain text rather than §-marked, neither side's grund check could say so. Every one of them is now gone from the shipped bytes or replaced by the declaration's public URL, and the grounding moved to the file that owns the payload rather than being deleted: integrations.rs carries a doc comment per embedded artifact citing what that artifact's comments cited, and the two strings are grounded in the source comment beside them. skills/ joins templates/ as an unwalked home (§FS-config.3.4.7) — the key is for exactly this content — so [citations.skill] goes with it and the generated entrypoint no longer renders a skills/ bullet for this repository. A new integration guard matches the declaration catalog against the embedded assets, their tree copies, the output goldens and the binaries' string literals, so a citation cannot ship again unnoticed, and the pre-commit lychee step now reads skills/ and templates/ as well, so the public URLs that replaced those citations are checked like every other link in the tree — neither home is walked by grund check, so nothing else would. Closes issue #156. (PR #183)

  • §FS-fmt.1, §FS-fmt.3, §FS-fmt.6.1, §FS-fmt.6.6: fmt --check and the implicit dry run now preview every line fmt --write would change, including automatically enabled Markdown cross-reference rewrites and the always-linkified kind-index carve-out, while write summaries accurately label their existing counts as changed lines rather than references. Fixes issue #82. (PR #180)