Repository navigation
grund 0.13.0
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 checkrun at all. Leaving the entry in[workspace] membersis 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_membersis the third path: a sibling ofmemberswith 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 theoptional_membersline rather than once per citation site. This is an opt-out, not a softer default: a member listed inmembersand 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 whoseproject_namedisagrees 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 bothmembersandoptional_membersis 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 inmembersis 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 agrund initscaffold 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 exits0— so the exit code carries nothing about coverage and the report has to. It withholds thesuccessmarker 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, andgrund config showprints it betweenmembersandinclude_root(§FS-config.3.8). It is additive with an empty default, sogrund_config_versionstays 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 = falsesays 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--fullwidens 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 citationscheck,check --fullandlistwere all silent and all exited0. Every command that resolves a block's member boundary —check,list,refs,cover,fmtand the rest — now prints one CLI-levelwarning:on stderr naming the block's owninclude_rootline, the tree no project scans, what that costs, and the two remedies: setinclude_root = true, or point another project's[scan] includeat 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 jsonwith no diagnosticcode, and never moves the exit code — but like every warning it stands in place of thesuccessmarker (§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:CheckOutputgains a publicunread_opted_out_blocks: usizefield 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 noDefault, so unlike theInitOptsentry 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 fmtis is written down once, and every clause of it now has a check. Twelvefmtdefects were each fixed as its own bug, and the fix commits kept independently rediscovering fragments of one sentence:fmtis the modelgrund checkverifies 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, withnothing was rewritten:named as the one licensed difference: §FS-fmt.3 spells a strict abort's line differently on purpose, because the two exit2s 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 newfmt-partial-scan-error-still-reportse2e 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 afmtwalk resolves shorthands and cross-references against are accepted only as aCompleteFindingsproof, 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, andscripts/check_fmt_complete_findings.shis the check that it stays one. The rule has a direction, and §7 says so: equivalence bindsfmttocheck's model, and nevercheck's findings to whatfmtis 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-levelwarning: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 = falseon 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.tomlform 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 thesuccessmarker, 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 nogrundcommand 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 --checkturns the--dry-runpreview 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 checkverifies the block's version rather than its bytes (§FS-check.3.5), andgrund init --dry-runsaw the drift, printedwould-update AGENTS.md, and exited0.--checkruns that same preview, prints that same report byte for byte, writes nothing under any flag combination including--force, and exits1when any reported path is awould-write,would-append, orwould-update; anote:and thenext:block never earn it, and a refused target still exits2.--dry-runitself deliberately keeps its0:1is the findings slot §FS-cli.5 freezes andinithad never used, so--checkfills a hole in the mapping, whereas moving--dry-runwould 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 agrund-init-checkpre-commit hook placed aftergrund fmt --writeso the block is measured oncefmthas had its say. Library callers:InitOptsgains a publiccheck: boolfield, an additive break for exhaustive struct literals in the same way--no-vcswas in0.10.1;..InitOpts::default()is unaffected. Closes issue #172. (PR #185) -
§FS-fmt.2.5, §FS-config.3.10:
grund fmtcan be suppressed per file and per region, without taking the citations out ofcheck. A new[fmt] excludelist names files the command performs no rewrite in, as gitignore-style globs against the config root; agrund:fmt offcomment line —<!-- grund:fmt off -->in Markdown, a[scan] comment_prefixescomment in a source file — suppresses the rewrite from the next line until the matchingonor 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 forgrund refsand the LSP. Nothing else changes — a suppressed file is still walked and still checked, sogrund checkreports exactly what it reported before, and the dry run and--writeagree 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 linegrund fmt --writerefuses 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 andfmtrefuses to repair. The key is additive with an empty default, sogrund_config_versionstays 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_groundingand a newgrounding_levelare[[kinds]]row keys, each with its[reference]twin as the default for rows that do not say — the shapeindexalready has (§FS-config.3.4.2), a global default with the row winning. So "every file underskills/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_levelis an integer in Markdown heading levels:1is the file, which is the unit every configuration had before the key existed,2adds every##subtree,6every 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-groundingand the global key stay one knob: the flag sets the same default, and an explicitfalseon a row still wins (§FS-check.1). Both keys are additive, sogrund_config_versionstays 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 amust 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]] prefixkey stops loading, at the release its warning named.prefixwas this key's name untilcitable = falsemade it wrong for half the table; 0.12.0 renamed it tokind, 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 spellsprefixis 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 exits2on it with an ordinary config error (§FS-config.4.3) anchored at the lineprefixis written on:error: grund.toml:4: [[kinds]] `prefix` was removed in grund 0.13.0 — rename it to `kind`. An entry that sets bothkindandprefixearns 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 onegrund 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_versionstays 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, itsdeprecated-config-keydiagnostic 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 the0.13.0in 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 as0.12.4under an error saying0.13.0. The release path now asks the question directly rather than through a test no publication path runs:scripts/check_release_ramps.pyreads the release each message names out of the tree's own message text — the sources undercrates/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, andauto-bump.ymlandrelease-minor.ymlrun it again before they push a candidate branch. On this tree it reports an empty window — at or above0.13.0for the removal, below0.13.0for 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.0the 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 exits1instead of0, the finding is on the error path and no longer stands in for thesuccessmarker as a warning, and under--format jsonthe diagnostic'sseverityreadserror— while itscodestaysmissing-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.0reports in its place — the past tense the sibling[[kinds]] prefixremoval 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 nogrundcommand 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 since0.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.6is 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.12renumber 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, andscripts/check_release_ramps.pyreads the release each message names out of the tree's own message text and refuses a version that contradicts it. So a0.12.4carrying this error — one minor before every run of0.12.0said 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 theprefixremoval already required at or above0.13.0while this warning still promised0.13.0and 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 a0.12.4is answered withthis 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 name0.14.0and are still ahead. Decided in §DF-index-compatibility-ramp. Closes issue #200. (PR #203) -
§AR-ci.9 / §AR-ci.1: the
fissileconfig 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.fissile0.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, andfissile checkthen 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.pylearns 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,
mainopens the next patch asX.Y.Z-devinstead of keeping the version it just published. A build frommainpreviously 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 checknow 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
docsfile-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, aSKILL.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, andrhei's largest spec measures 1482 lines — 18 lines under 1500, which is one edit. 2000 is not a new number either; it is the ceilingrhei's ownAR-source-file-size.1already 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 indocs/file-size-agent-exceptions.tomlordocs/file-size-human-exceptions.tomltargets thedocsrule, so raising the limit strands none of them. Closes issue #174. (PR #175) -
Schema: the
AGENTS.mdmanaged block moves to v8 (§FS-init.2.3.5, §DF-directions-render): the generated### Citation directionssection 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 somust = ["FS|GOAL", "AR"]reads(FS or GOAL) and ARrather than the opposite grouping, three or more alternatives take the Oxford comma,*/ARis said asAR in any projectinstead of leaking rule grammar, a closed per-kind default folds intomay cite only …, a legend says which levels gate and which suggest, and the closing line reports the global default instead of pointing back atgrund.toml. With[reference] require_groundingon the paragraph also carries the grounding sentence the block never rendered, under[citations]or without it. Migration: rungrund init— until thengrund checkreports oneagents-initfinding (§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 fromvjovanov/rhei#184on, keeping.agents/rhei/as a deprecated fallback it warns about. The reason is the one that movedfissile.tomlin 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 nogrundbehavior changes — the directory is outside[scan] includeat either name. A rhei older than that change does not look under the new home, so instantiating this template needs a rhei built frommainor later. (PR #199)
Deprecated
- §FS-config.1.2, §FS-check.4.11: a repository whose config lives at
.agents/grund.tomlis now told to move it to the repository root. The root is where agrund.tomlbelongs, and the reason is visibility (§DF-config-file-location.2.4): a dot-directory is hidden fromls, 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 — sogrund check,grund config validateandgrund config shownow print one CLI-levelwarning:on stderr naming the file the run read and the baregrund.tomlbeside it that should hold it, both rendered relative to the config root so the line reads as thegit mva 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 andlist,refs,cover,fmtand 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 jsonit keeps its text with the codedeprecated-config-locationand nullpath/line/sites. The exit code never moves, but like every warning the line stands in place of thesuccessmarker (§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.tomlstops 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/.grundtakes no part in that move and adds no.agent-grounds/grund.tomllocation: its config left the agent directory already, and for the stronger reason. Thecheckandconfighelp 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-shippingfor an ID thevendoredmember holds reported a bareID not found:and pointed atgrund 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 andgrund <ID>still exits1with empty stdout: §FS-workspace.4 is whatgrund checkenforces at a citation site, so a query command that printed the body would teach the reader a citation CI rejects, and one resolver acrosscheckand 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, joineda, 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 thegrund listhint 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 jsonstill carries"code":"not-found", with the candidate insidemessage. 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-fencescited 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 thingfmtleaves 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'sspec.refsmoves off the staleFS-005-fmt.2.3spelling 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] extensionsis consulted, while every section that wrote the walk's skips down spoke of hidden directories, so adocs/.notes.mdcarrying a dangling citation was invisible togrund checkand togrund check --fullalike 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 hiddenincludeentry and a[[kinds]]filehome with a hidden name are read by nobody, which is the exception §FS-check.1.3's "a gitignored, excluded, or hiddenincluderoot is read by the plain run" needed in order to stay true — and--fullremains 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.mdused to answerno files under … matched grund's extensions (md, rs, go, …)about a file whose extension ismd: 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. Theempty-scandiagnostic code, the stderr stream, and the exit code0do 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 yieldssuccessand exit0fromcheck, from--full, and when[scan] includenames 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 fmtrun 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 docsthen 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 withoptional_membersabove, 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 newworkspace-fmt-narrowed-boundary-symlinke2e 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 checknarrowed 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"]besideinclude = ["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 listprinted nothing and exited0; onlygrund checksaid 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-levelwarning:on stderr —check,list,refs,cover,fmt, and the commands that resolve a boundary without walking it,config validateandinit— at that block'smembersline, 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 ownmembersline. One run spells the whole tree from one place, the directory it was launched in:grund initinside 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.tomlandsub/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] includekey 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 nogrundcommand repairs it: the fix is a judgement between repointing[scan] includeand 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 names0.14.0as 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 memberslists omit the canonical project whose own entrypoint is being rendered, instead of linking that project back to its ownAGENTS.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 = falsekeeps 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=1used 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\nto\nwhile 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_expectedfolds 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 formtext eol=lfand every end-with-a-newline convention expect, and it keeps\ No newline at end of fileout of a diff a reviewer reads as bytes.goldens_are_in_canonical_formwalks every case undertests/e2e/cases/andexamples/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
grundships or prints names an ID ofgrund's own repository. Thegrund.tomlthatgrund initwrites opened by citingFS-config.3.9; the skillgrund agent-setup-instructionsprints byte-for-byte carried a dozen citations behind../../docs/…paths; the payloadsgrund integrations --writeinstalls into a user's dotfiles carried three dozen more, permanently, since--writeis idempotent; and two printed strings —grund help configand the homeless-kind config error — ended in one each. None of those resolve in the repository they land in, and a project with anFS-configof its own resolves them to an unrelated document; because they are plain text rather than§-marked, neither side'sgrund checkcould 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.rscarries 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/joinstemplates/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 askills/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-commitlycheestep now readsskills/andtemplates/as well, so the public URLs that replaced those citations are checked like every other link in the tree — neither home is walked bygrund 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 --checkand the implicit dry run now preview every linefmt --writewould 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)