fix(validator): bind the SSoT ignore guard to git instead of .gitignore text - #423
Merged
Conversation
…re text The `.gitignore preserves persistent SSoT artifacts` check compared whole `.gitignore` lines against a fixed list of directory paths. A downstream fork's `.agentcortex/context/archive/*.md` hides the archived Work Logs without ever spelling that directory, so the guard reported PASS while the governance record stopped being committed. Reproduced before fixing: one appended line left `validate.sh` at `fail=0` while `git check-ignore` confirmed the logs were hidden. The same blind spot had grown independently in both forks of the same ancestor, which is what makes it a class rather than a slip. The check now asks git, and four of its flags carry the correctness: - probes a representative FILE inside each protected artifact -- a directory probe reproduces the original blindness, since `docs/specs/*.md` ignores the contents without ever matching the directory - takes the verdict from `check-ignore -q`, never `-v`. `-v` exits 0 whenever a pattern MATCHED, negations included: on the ordinary `docs/adr/*` + `!docs/adr/*.md` idiom `-v` exits 0 while `-q` exits 1 and git tracks the file, so reading `-v` would fail a correct adopter and name their protective `!` line as the pattern to remove - passes `--no-index`, without which `check-ignore` skips tracked paths and `current_state.md`, the one real probe, is inert in every healthy deploy - is tri-state: git absent or not a work tree reports SKIP, never assurance - is no longer gated on `.gitignore` existing; that branch emitted a PASS asserted without looking at anything, while `.git/info/exclude` and a global excludes file hide files just as effectively An outer repository hiding the whole tree gets its own cause and remedy, but by re-labelling after the probe loop, never deciding before it: all probes ignored AND a non-empty `rev-parse --show-prefix`. Deliberately not `check-ignore -- .` -- a blank CRLF line is the pattern `\r`, which git strips to the empty string, and the empty pattern matches the pathspec `.`, so that discriminator failed every `core.autocrlf=true` checkout including this repository. `audit-guardrails.md` Test 1 asserted `git status` shows none of `.agent/`, `.agents/`, `.antigravity/`, `.agentcortex/context/`; a real cold deploy shows 58 / 29 / 1 / 2 paths, and its command did not run at all (`--force` is not a `deploy.sh` flag). Rewritten to the verified ignore set, re-pointed at the canonical `deploy.sh` so a downstream reader is not sent to the network and handed an `.agentcortex-src/` cache in their own tree, and now bound by a test that executes the page's own assertion list against a real deployed ignore block. Both language twins; heading kept verbatim because it is an encoding canary in both validators. Adopter delta, measured on one identical tree with each version's own deployed validator: with `.gitignore` hiding both the archive and the adopter's own spec, v1.8.23 prints `[PASS]` and `fail=0`; this build prints `[FAIL] 2/6 probes ignored` naming each `source:line`. On a healthy tree the tallies are unchanged, so this adds detection without adding churn. 16 test arms, every new guard mutation-proved against the code it guards. Full CI-equivalent suite (no `-m` filter, run as CI runs it): 913 passed, 1 skipped, 0 failed. Both validators on this repo `pass=118 warn=3 fail=0 skip=2`, identical, exit 0. ADR-006 native-check ratchet unmoved at 204/204 -- the stricter guard cost no headroom. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Row 185 described the CI markdown-link checker's scope by quoting the syntax
it resolves. The checker's own regex extracts a bracketed-then-parenthesised
target wherever it appears -- code fencing does not exempt it -- so the example
became a link pointing at a file named `path`, and Check Markdown Links went
red in 5s.
Reworded to prose. No other broken links: re-checked every tracked .md with a
working extractor.
Worth recording why this was not caught before pushing: reproducing the CI step
locally passed vacuously. The step is `grep -oP ... 2>/dev/null`, and `grep -oP`
fails on this machine ("-P supports only unibyte and UTF-8 locales", exit 2),
so the suppressed error made every file yield zero links. A local reproduction
of a check has to be shown capable of failing before its green means anything --
which is the same defect the parent commit exists to fix.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
KbWen
added a commit
that referenced
this pull request
Aug 24, 2026
…times learning how (#424) Release cut for **v1.8.24**, packaging [#423](#423). ## What is in it A sibling fork of the same ancestor reported that `.gitignore preserves persistent SSoT artifacts` returned PASS while the governance record had stopped being committed — their `.agentcortex/context/archive/*.md` hides the archived Work Logs without ever spelling that directory, and the guard compared whole `.gitignore` lines against a fixed list of directory paths. **The same blind spot had grown independently in both forks**, which is what makes it a class. The guard now asks git. The fix then broke three times, each break the same shape as the defect it was fixing, and none of them caught by self-review: | break | why it mattered | |---|---| | verdict read from `check-ignore -v` | `-v` exits 0 whenever a pattern **matched**, negations included — so `dir/*` + `!dir/*.md` failed a correct adopter and named their protective `!` line as the pattern to remove | | `--no-index` omitted | `check-ignore` skips **tracked** paths, so the one real probe was inert in every healthy deploy — a detection *narrowing* inside a broadening change | | discriminating on `check-ignore -- .` | a blank CRLF line is the pattern `\r`, git strips it to the empty string, and the empty pattern matches `.` — FAILing every `core.autocrlf=true` checkout, **including this repository** | The third survived a self-review, seventeen scenarios and a green 912-test suite, because every scenario deployed a fresh tree and `deploy.sh` writes LF. **A fresh-deploy fixture is not a checkout.** ## Adopter delta On a healthy tree, nothing changes — measured: upgrading a v1.8.23 install carrying its own work log, spec and ignore rule left the tallies **identical** at `96/6/2/6`. Newly caught, each a FAIL naming the exact `source:line`: a `.gitignore` hiding the archive or `docs/specs/` by a content glob; a `.git/info/exclude` or global excludes file doing the same (previously not consulted at all); a project deployed under an outer repository's ignored path (its own cause, its own remedy, and an explicit warning not to delete that rule). Newly **not** flagged, equally deliberate: the `dir/*` + `!dir/*.md` idiom, a CRLF `.gitignore`, and a non-git tree — which reports SKIP rather than a false PASS. All four changed deployed files are `core` tier, so this arrives on the next deploy with no adopter action. No new flags, no new deployed files, no engine or gate change. ## Release surfaces Seven, bumped by hand: `deploy.sh` `ACX_VERSION`, `CITATION.cff`, both `TESTING_PROTOCOL` twins, both `AGENT_MODEL_GUIDE` twins, and the CHANGELOG heading. There is still no test pinning their consistency — recorded, not fixed here. ## Evidence Full CI-equivalent suite (no `-m` filter, run as CI runs it): **913 passed, 1 skipped, 0 failed**. Both validators on this repo `pass=118 warn=3 fail=0 skip=2`, identical, exit 0. ADR-006 native-check ratchet **unmoved at 204/204** — the stricter guard cost no headroom. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A sibling fork of the same ancestor reported six governance findings. Diagnosis against this tree confirmed one root cause worth fixing here, at two altitudes — and the same blind spot had grown independently in both forks, which is what makes it a class rather than a slip.
The defect
.gitignore preserves persistent SSoT artifactscompared whole.gitignorelines against a fixed list of directory paths..agentcortex/context/archive/*.mdhides the archived Work Logs without ever spelling that directory, so the guard reported PASS while the governance record stopped being committed.Reproduced before fixing — one appended line,
fail=0, andgit check-ignoreconfirming the logs were hidden:The fix, and the three regressions it took to get right
The check now asks git. Four flags carry the correctness, and each of the last three was a regression I introduced and an independent reviewer caught:
docs/specs/*.mdignores contents without matching the directory-q, never-v-vexits 0 whenever a pattern matched, negations included. Ondocs/adr/*+!docs/adr/*.md,-vexits 0 while-qexits 1 and git tracks the file — reading-vwould fail a correct adopter and name their protective!line as the pattern to remove--no-indexcheck-ignoreskips tracked paths, socurrent_state.md— the one real probe — is inert in every healthy deploy. A detection narrowing inside a change meant to broadenrev-parse --show-prefix. Deliberately notcheck-ignore -- .: a blank CRLF line is the pattern\r, git strips it to the empty string, and the empty pattern matches.— that discriminator failed everycore.autocrlf=truecheckout, including this repositoryAlso: no longer gated on
.gitignoreexisting (that branch emitted a PASS asserted without looking at anything, while.git/info/excludeand a global excludes file hide files just as well), and tri-state so a check that could not run reports SKIP rather than assurance.The playbook is now bound to the mechanism
audit-guardrails.mdTest 1 assertedgit statusshows none of.agent/,.agents/,.antigravity/,.agentcortex/context/. A real cold deploy shows 58 / 29 / 1 / 2 paths — and its command did not run at all, since--forceis not adeploy.shflag and the catch-all*)takes it as TARGET.Rewritten to the verified ignore set, re-pointed at the canonical
deploy.sh(the wrapper clones from the remote and writes an.agentcortex-src/cache into the reader's own tree — this guide ships downstream), and bound by a test that executes the page's own assertion list against a real deployed ignore block. Both language twins; heading kept verbatim because it is an encoding canary in both validators.Adopter delta
Measured on one identical tree with each version's own deployed validator,
.gitignorehiding both the archive and the adopter's own spec:[PASS] .gitignore preserves persistent SSoT artifacts,fail=0[FAIL] 2/6 probes ignorednaming eachsource:lineOn a healthy tree the tallies are identical before and after upgrade, so this adds detection without adding churn. All four changed deployed files are
coretier — adopters get it on their next deploy with no action.Evidence
--no-pythonhost · non-git tree · nested under an outervendor/ignore · upgrade from v1.8.23 with downstream state seeded · re-deploy ×3 · rollback · no-trailing-newline.gitignore· git worktree · the pre-commit hook's real interpreter (powershell5.1, notpwsh) · CRLF.gitignore-mfilter, run as CI runs it (--splits 3): 913 passed, 1 skipped, 0 failedpass=118 warn=3 fail=0 skip=2, identical, exit 0What this does not do
The parity and structural guards live in non-required CI contexts, so they fail visibly on a PR and nothing more; making one blocking is a branch-protection setting, not something this branch can reach. Pytest is never exercised under the 3.9 floor — CI's 3.9 job runs
validate.shonly — so 3.9 compliance here rests on the static ratchet plus a scan, not on ever running these files under 3.9. And the probes are representative, not exhaustive: a pattern narrower than the probe name (archive/*-worklog.md) still slips through. It catches the whole-directory and whole-extension shapes, which is the class that bit downstream.Four findings surfaced by doing the work were filed rather than folded in: #183 workflow job-graph
needs:integrity, #184 PowerShell version floor + a 5.1 CI arm, #185 backtick-path reference integrity, #186token-governance.md §8's compaction target colliding with the archived-Work-Log contract.🤖 Generated with Claude Code