Fourth review round on #646.
1. GraphKey returned early on an empty repo prefix, before filepath.FromSlash.
A standalone index carries no prefix but its keys still use native
separators, so a '/'-spelled forge or git path missed them on Windows and
get_pr_impact / suggest_reviewers(number=...) silently lost changed symbols
and reviewer signals. Only GraphKeyedPath returns unchanged now;
RepoRelativePath always converts, then takes the prefix when one applies.
2. CODEOWNERS received a graph key in ids mode.
relForRepo only strips an absolute repo root, so repo-a/pkg/auth/login.go
reached MatchFile with the prefix still on and a root-anchored rule such as
/pkg/auth/ matched nothing. matched_files reported the same spelling back to
the caller. Both now go through the new analysis.RepoRelPath, GraphKey's
inverse: it strips the prefix only for a GraphKeyedPath, because that is the
one domain that carries it by construction.
3. Tests.
- Empty-prefix multi-segment cases for GraphKey and JoinFileNodes. The
previous a.go fixture has no separator, so it cannot fail either way.
- The ids CODEOWNERS fixture is now root-anchored (/pkg/auth/). The
unanchored rule also matched the graph-keyed spelling and masked the bug.
- TestJoinHunksToSymbolsPrefixedDeleteAndRename covers the two hunk-less
change kinds against a prefix-shadowed graph: the delete's Path and the
rename's PreviousPath must resolve the nested old-side key and never the
same-named shadow. Their existing tests use an empty prefix and so did not
bind this contract.
- The Windows selector gains PrefixedDeleteAndRename.
Each fix is sabotage-verified separately: forcing the ids branch back to a
raw graph key leaves only the CODEOWNERS-less reviewers, passing the graph
key to MatchFile drops the codeowner signal entirely, and declaring the
vanished path GraphKeyedPath makes the delete/rename test miss both old-side
symbols.
Four fixtures needed the same correction as TestChangedSymbolsForFiles did
earlier: prToolsTestServer, conflictsTestServer and conflictsBudgetServer
hard-coded '/'-spelled graph keys while supplying the same strings as forge
paths, which describes an index a Windows daemon never writes. They now store
native and supply the forge spelling, which is the shape under test.
Windows: internal/mcp 27 -> 24 against main, failing test names diffed,
newly-broken set empty. internal/analysis passes clean. internal/review keeps
its four pre-existing failures. Lint: 6 staticcheck findings, pre-existing.
Third review round on #646.
1. suggest_reviewers(ids=...) double-prefixed graph-keyed paths.
resolveReviewerChangeset's three sources do not share a vocabulary: ids
returns graph node FilePaths, base (git) and number (forge) return
repo-relative paths. Forcing analysis.RepoRelativePath on all three
looked up repo-a/pkg/auth/login.go as repo-a/repo-a/pkg/auth/login.go, so
the ownership and co-change signals disappeared while CODEOWNERS - which
matches the repo-relative spelling - still answered and the tool looked
healthy.
The resolver now returns the domain with the files and the two lookups
use it. That also gives GraphKeyedPath its first production caller.
My round-two audit missed this: I read the call site's comment, which
says the paths are repo-relative, without walking all three producers
feeding it. The comment was true for two of them.
2. File risk stripped the prefix after filepath.Clean.
fromGraphKey called cleanPath first, and cleanPath ends in filepath.Clean,
so on Windows repo-a/repo-a\widget.go became repo-a\repo-a\widget.go and
the '/'-joined prefix no longer matched. Changed-symbol impact stayed on a
graph-prefixed row while findings and ChangedFiles produced a second
repo-relative row for the same file. Both domains now normalize through
graphpath.Norm before the strip and leave in the documented repo-relative
'/' spelling.
This also fixes three pre-existing Windows failures in internal/review:
TestRankFileRiskUsesImpact, TestRankFileRiskNormalizesRepoPrefix and
TestRankFileRiskExemptsTestFilesFromCoverageDebt. The package goes from
seven failures to four; the remaining four are untouched by this change.
3. Windows coverage.
The selector gains GraphKey, JoinFileNodes, ChangedSymbolsForFiles, both
prefix-shadow end-to-end tests, the new ids test and RankFileRisk, and
./internal/review joins the package list.
Tests. The new prefixed-graph ids test asserts all three reviewer signals
survive; the existing ids tests use an unprefixed graph where both domains
coincide, which is why they could not see the regression. Both
prefix-shadow end-to-end tests now require exactly one FileRisk row equal
to the changed file - the previous NotEqual(shadow) assertion passed even
when both erroneous rows were present.
Each fix is sabotage-verified separately: restoring the ids branch to
RepoRelativePath leaves only the CODEOWNERS reviewer, and restoring
cleanPath-before-strip produces exactly the two rows described above.
Also audited, and clean: every other JoinFileNodes / GraphKey caller.
changedSymbolsForFiles is fed only by forge PR file lists through
resolvePRFiles and prImpactForNumber, so its repo-relative domain is
correct. The only two remaining repoPrefix strips both normalize first and
both have an established domain.
reviewRulepackMatches builds the changed-file key correctly and then
looks it up with filepath.Clean, which rewrites the one separator a graph
path keeps as '/': the repo prefix. Probed on windows/amd64:
changed map key "repo-a/pkg\widget.go"
target.GraphPath "repo-a/pkg\widget.go" <- identical
filepath.Clean(GraphPath) "repo-a\pkg\widget.go" <- prefix slash lost
hit false
So `review` narrowed the rulepack to an empty target set and reported
zero findings on code its own detector bundle flags — the same false
clean the join was written to fix, reintroduced by the Clean.
Compare in graphpath.Norm form on both sides instead. Norm is the
canonical comparison spelling for a path that is "<prefix>/" plus a
native remainder, and it is filepath.ToSlash, so this is the identity on
POSIX.
Second half, same contract: reviewRepoRelPath feeds the `.gortex.yaml`
rule globs, rankFileRisk's row keys and the forge comment API, all of
which speak '/'. It returned `pkg\widget.go` on Windows, which matches no
glob and anchors no comment. Normalize there too.
Whole package on windows before/after: 27 -> 24 failures, exactly the
three rulepack tests flip, nothing else changes state. Each half is
separately load-bearing: reverting the join fails all three, reverting
the output normalization fails the two that assert m.File.
graphpath.Norm is deliberately a no-op on POSIX — a backslash is an
ordinary filename byte there — so no test can bind this on the
linux/macos matrix. The three names join the existing windows
native-separator step, whose package list already carries internal/mcp.
TestFeedbackDir, TestParseDiffGitPaths and TestParseDiffLinesNewSide all
compare against a '/'-spelled path while the value under test is a native
filesystem path. On POSIX the two coincide, so the linux/macos matrix has
never seen it; on windows/amd64 all three fail:
"\home\user\.cache\gortex\5887dcec1741_latest"
does not contain ".cache/gortex"
parseDiffGitPaths("diff --git a/pkg/foo.go b/pkg/foo.go")
= "pkg\foo.go", want "pkg/foo.go"
diffmap_test.go:63: expected new-side lines for pkg/foo.go
The production values are correct in each case and are left alone:
- FeedbackDir is filepath.Join(cacheDir, key) and feeds os.Open, so it
must carry native separators.
- cleanDiffPath is filepath.Clean, and JoinFileNodes looks a diff path
up as repoPrefix + "/" + path. Indexed paths are repoPrefix + '/' +
the rest in native separators (see internal/graphpath), so the
cleaned native form is exactly what the join needs. Making diff paths
slash-canonical would break that lookup on Windows.
So the fix is on the assertion side: build the expectation with
filepath.Join / filepath.Clean instead of a literal. On POSIX both are
identity for these inputs, so the assertions keep exactly the strength
they had on the matrix that already runs them.
Fold the three into the existing native-separator windows step rather
than adding one: internal/analysis, internal/mcp, internal/resolver and
internal/graph/store_sqlite are already in that step's package list, and
internal/persistence is already compiled by the sidecar step above it.
goreleaser ran `go mod tidy` as a before-hook inside the release container.
It took v0.63.8 down: the hook spent 4m49s walking the module graph and then
failed when proxy.golang.org returned HTTP/2 INTERNAL_ERROR on six unrelated
fetches. `go mod tidy` resolves the test dependencies of dependencies too, so
it touches far more of the network than a build needs, on the one path where
a flake costs a release.
The hook was also unsound in a quieter way. It MUTATES go.mod/go.sum, so a
drifted module set would have been silently rewritten inside the container
and the published binaries built against something no CI job had ever
compiled. A release-time rewrite is not a check.
Drop it — the config already skips tests here on the reasoning that the tag
is on a green commit, and the same argument applies.
Nothing verified module tidiness before this: no workflow, no Makefile
target. So the `lint` job gains a `go mod tidy` + `git diff --exit-code`
step, which fails the PR that introduces drift instead of papering over it
at tag time. main is already tidy, so the gate is green on landing.
Verified with the real toolchain, not by reading it:
* `goreleaser build --snapshot` with a deliberately failing before-hook
aborts at "running before hooks" in 0s, so hooks do execute in that mode;
with `hooks: []` the stage never appears and the run goes straight from
snapshotting to building. No default hook takes its place.
* `goreleaser check` validates the config.
* The CI gate passes on main unchanged, and fails with exit 1 (naming
go.mod) against a commit carrying a deliberately untidy require line.
newPlanLockFixture opens an on-disk Store and never closes it. Windows
refuses to unlink an open file, so plan_lock.sqlite survives the test and
the enclosing t.TempDir() cleanup fails.
Measured on windows/amd64, go1.26.6, whole package before the change:
--- FAIL: TestSweepPlanLocks
--- FAIL: TestAdjacencyPlanLocksDuringBulkLoad
--- FAIL: TestSweepPlanLockReceiverRebindBatch
--- FAIL: TestPreparedStatementPlansNeverScanBigTables
--- FAIL: TestSweepWarnPlanLocks
testing.go:1464: TempDir RemoveAll cleanup: unlinkat
...\plan_lock.sqlite: The process cannot access the file because
it is being used by another process.
Five failures, one cause, zero failing assertions — the fixture is the
only holder. After the change the package is ok, 147s, no failures.
Register the close with t.Cleanup after t.TempDir() so LIFO ordering
closes the database before the directory is removed. This is already the
convention at 160 of the 165 Open() sites in the package's tests.
Guard it on the windows runner. Every assertion in these tests is
platform-neutral: on linux/macos an unlinked-but-open file is removed
normally, so the whole set passes with or without the Close and the
matrix cannot protect the fixture from losing it again. Same reasoning,
and the same shape, as the sidecar-handle step added in #415.
The generated cask used `pre_install` / `post_install`. Those are formula
DSL methods; a cask has no such stanzas, so brew rejected the file at load
time with
Error: Cask 'gortex' definition is invalid: undefined method 'pre_install'
Every brew command that touches the cask parses it, so `brew install` and
`brew upgrade` both failed for every macOS user on both arches, starting
with v0.63.7. Closes#639.
Two further defects made a straight rename insufficient:
* The cask DSL's `system_command` is `SystemCommand.run!`, which raises on
a non-zero exit. `gortex daemon status` exits 1 exactly when no daemon
is reachable, so `next unless status.success?` could never run — brew
would have aborted the install outright on any machine without a live
daemon. The probe now passes `must_succeed: false`.
* `preflight` was the wrong hook for stopping the daemon. On upgrade brew
unlinks the old cask's binary (`start_upgrade` -> `uninstall_artifacts`)
before installing the new cask's artifacts, so by the time a preflight
block runs there is no gortex on disk to ask. The stop half could never
fire on the path it was written for.
`postflight` handles both halves instead: after the new binary is linked,
probe for a daemon and, only if one answers, `daemon restart` — which stops
the old process (blocking until it exits, releasing the store lock) before
starting the new one, so a store migration still runs alone. A fresh install
and CI have no daemon answering and skip it.
The cask body moves out of the release.yml heredoc into
.github/homebrew/gortex.rb.tmpl, rendered by scripts/render-cask.sh. That
makes it reviewable as Ruby, removes the shell-expansion hazards of an
unquoted heredoc, and — the point — lets a real brew load it before
publication. The renderer also refuses a malformed sha256, a version with a
leading "v", or a placeholder that survived substitution. Rendered output is
byte-identical to the published cask apart from the hook block.
Nothing could have caught this: the file was valid Ruby, valid YAML, and the
tap has no CI, so the first machine to evaluate the cask was a user's.
scripts/validate-cask.sh renders the template with dummy values and loads it
through a real brew. It runs on every PR that touches the cask
(.github/workflows/homebrew-cask.yml) and again in build-darwin as the last
gate before the release job pushes to the tap. Both are macOS-only —
Homebrew on Linux cannot load casks. Verified by reintroducing `pre_install`
in the template: the validator fails with the exact error users reported.
verify-signatures skips actions/checkout on purpose — it verifies
published bytes, not the source tree — so the job has no git remote for
gh to infer the repository from. `gh release download` aborted with
"failed to run git: fatal: not a git repository" before downloading
anything, and the gate failed the v0.63.6 release despite that release
being sound.
Pass --repo "$GITHUB_REPOSITORY" so the lookup does not depend on a
checkout.
The gate was originally rehearsed against a stubbed gh, which could not
surface a repository-resolution failure. It is now exercised end to end
against the published v0.63.6 release with the real gh and cosign: all
12 signatures verify, and re-appending a line to the signed
checksums.txt makes the gate name checksums.txt.sig and exit 1.
checksums.txt is assembled across three jobs — goreleaser, then darwin,
then windows — and signed along the way. Nothing checked that the
signature a release ships still matches the bytes it ships, so a step
that rewrote a file after signing it went unnoticed for 28 releases.
Add a gate that runs after every job which can touch a release asset,
re-downloads the release, and verifies each published .sig against the
published artifact with the same cosign command consumers are told to
use. It also asserts that every primary artifact is both signed and
listed in checksums.txt, the two verification paths consumers rely on.
Rekor lookups are a network call against a shared service, so a
verification failure is retried twice before the release is failed; a
genuine mismatch fails deterministically and still stops the release.
The release job signs checksums.txt once it has folded in the darwin
tarballs. release-windows then downloads that signed file, appends the
windows zip's sha256, and re-uploads it with --clobber — but never
re-signs. The published checksums.txt.sig therefore covers the file as
it was before the windows line existed, and verification fails on a
release whose artifacts are all authentic.
Every release from v0.35.3 to v0.63.5 shipped this way. Stripping the
trailing windows line from a published checksums.txt makes its
published signature verify, which pins the cause exactly.
Per-artifact signatures were always correct, and install.sh verifies
the asset's own .sig/.pem rather than the checksums file, so installs
were unaffected — but the conventional "verify checksums.txt, then
check everything against it" supply-chain gate fails closed.
Make the last writer of checksums.txt also its signer: append, sort,
re-sign, and clobber the file plus its .sig and .pem together. Keyless
signing here produces the same certificate identity as the release job
(same workflow ref, same tag), so the documented verification command
is unchanged.
Also stop the append from failing open. `gh release download ... ||
: > checksums.txt` turned a transient download failure into an empty
file, which the subsequent --clobber would publish as a windows-only
checksums.txt, breaking sha256 verification for every other platform.
needs:release guarantees the file exists, so treat a failed download
as fatal.
The generated cask gains guarded pre_install/post_install hooks: pre stops
a running daemon (only when one answers; recorded via a tmp marker) so the
binary swap and any store migration happen without a stale daemon holding
the old image and the store lock; post restarts onto the new binary only
when one was running. Fresh installs and CI never launch a daemon.
Fixes the brew-upgrade leg of #609 alongside PR #611's detection layers.
The store_sqlite package under race and coverage sits just under the old
twenty-minute wall on Linux runners, and the localization projection tests
this branch adds push it past. Thirty minutes matches how long the suite
actually needs; the per-test alarms still catch a genuine hang early.
Adds the ~/.copilot sentinel and the copilot-cli expectation, and notes
that the list is hand-maintained so the next adapter is covered only
when both places are updated.
Indexed paths are repoPrefix + '/' + the rest in the indexing machine's
native separators, so a stored path on Windows reads `repo/dir\file.cs`.
Slash-only prefix logic applied to that shape sees the whole repo as one
directory: every `analyze path_prefix`, guard rule, doc filter and trigram
scope silently matches nothing below the repo root.
Three such filters were normalized when graphpath landed; the audit that
found them named "the remaining members" but only covered the reference
implementations in graph.go. Thirty-five were left raw. This finishes the
sweep through a single graphpath.HasPrefix helper, which also absorbs the
`prefix != ""` guard every call site repeated.
The helper deliberately keeps raw string-prefix semantics rather than
becoming segment-aware: `internal/mcp` matched `internal/mcpx` before and
still does. Making it segment-aware would quietly change what every
path_prefix argument returns, which is a different change from this one.
Left alone on purpose:
- `external::` / `external-call::` node sentinels, which are not paths
- comparisons against `repoPrefix + "/"`, where the separator after the
repo prefix is '/' by the storage invariant
Both of the earlier Windows fixes shipped with tests that cannot fail on
the runners that execute them: on POSIX a backslash is an ordinary filename
byte, so filepath.Clean and ToSlash leave the fixtures untouched and the
assertions pass with or without the production code. The build-windows job
already exists for exactly this class of platform-divergent semantics, so
the separator tests now run there — the only place they can go red.
Also simplifies the import-adjacency canonicality guard: `cleaned != path`
was redundant ahead of the normalized comparison, since equal normalized
forms already imply the two spellings differ only in separators.
A HOME-only override is a no-op only on Windows, so the helper that
fixes it and the resolver it protects are unverifiable on the linux and
macos matrix. Run both packages on the Windows job.
This is also the first time ./internal/platform runs there at all,
despite the package already shipping a Windows-only test file.
The guard's behaviour is more platform-dependent than most of this
codebase, and Windows was the platform not exercising it. Go maps a
symlink reparse point to ModeSymlink, which the guard keys on, but a
directory junction maps to ModeIrregular instead — the walk still refuses
to descend one, because Go treats both as name surrogates and withholds
ModeDir, so that difference is safe rather than a hole. Path case is the
other divergence: evalSymlinks normalises it on Windows, which is what
lets the containment test compare a resolved target against a resolved
root without a case-folding step of its own.
None of that was covered by an executing test. The Windows job deliberately
skips the full suite, so this adds only the guard's own package — pure Go,
no cgo, a fraction of a second — alongside the agent and sidecar tests
already there for the same reason.
Symlink creation on Windows needs SeCreateSymbolicLinkPrivilege, so the
tests skip rather than fail when the host will not grant it; on every other
platform a failure to create one stays a hard error.
Review catch: nothing in CI exercised any of this. On linux/macos the modified
tests pass whether or not the handle is released, because POSIX unlinks an open
file; the Windows job only built and ran internal/agents. So the fix was
unverified exactly where it matters.
TestCloseSidecar_ReleasesTheHandleAndStaysHarmless covers the four cases:
- closing a path that was never opened creates neither the database nor its
parent directory (the reason this is not OpenSidecar(path).Close())
- open -> CloseSidecar -> reopen returns a different, usable store — usable
meaning it serves a query, not merely that it is non-nil
- closing repeatedly is harmless
- the containing directory can then be removed
The reopen case is what gives the test teeth everywhere: with CloseSidecar
stubbed to return nil the cache still holds the old store, so the reopen hands
back the same pointer and the assertion fails on any platform. The RemoveAll
case is the one that only bites on Windows, and it now runs there.
Wired into build-windows next to the existing agents step, with the asymmetry
spelled out in the comment so the reason the test lives there is not lost.
Also hardened CloseSidecar per the non-blocking note: the entry is dropped
under the same lock that found it, so a concurrent OpenSidecar misses the cache
and opens a fresh handle rather than receiving one that is about to close.
Close still runs outside the lock, since it takes sidecarMu itself to clear any
other keys pointing at the same store.
Verified by sabotage: stubbing CloseSidecar to a no-op turns the reopen case
red. golangci-lint v2.11.4 clean, -race clean; internal/persistence reports one
failure, TestFeedbackDir, which fails identically on a clean origin/main (a
path-separator case, same class as #411, untouched here).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The release-time build hook fails a tag that produced a dynamically linked
binary, which is late. Build with the release link flags on every change and
run the same assertion, then execute the result on debian:11 (glibc 2.31,
the userland that reported the GLIBCXX failure) and alpine (no glibc at all)
so a missing netgo/osusergo tag surfaces here rather than for a user.
Also run the languages tests under static_link — that code path is compiled
out of every other job, so its tests would otherwise never execute anywhere.
The linux release was dynamically linked despite the README promising a
single static binary with no dependency chain. goreleaser-cross:v1.26 is
Ubuntu 24.04 / GCC 13.3, so the artifacts carried that host's floor —
GLIBCXX_3.4.32 and GLIBC_2.34 — and refused to start anywhere older:
gortex: /lib/x86_64-linux-gnu/libstdc++.so.6:
version `GLIBCXX_3.4.32' not found (required by gortex)
Debian 12 caps at GLIBCXX_3.4.30 and Ubuntu 22.04 at 3.4.29, so in practice
the binary only ran on Ubuntu 24.04+/Debian 13+. libstdc++ is in the link at
all because some tree-sitter grammars ship C++ external scanners. Windows
already linked -static; only linux was left dynamic.
Link -static with netgo, osusergo and static_link, which between them remove
every dlopen the binary would otherwise perform at runtime.
verify-static-elf.sh asserts no DT_NEEDED, no PT_INTERP and no symbol-version
requirements. It runs as a goreleaser build post-hook rather than a workflow
step because `goreleaser release` builds and publishes in the same run — a
check after it would only ever be a post-mortem on an already-published
artifact. The release job re-asserts it on the packaged tarballs and then
proves it by execution on debian:11 and alpine.
Both steps relied on go test's silent 10-minute default, which internal/mcp
was starting to exceed on ubuntu-latest, failing CI runs with no actionable
error beyond a goroutine dump.
The embeddings_gomlx XLA build failed to link on a clean Linux runner with
'cannot find -ltokenizers': hugot's XLA session uses the rust tokenizer, which
is statically linked as libtokenizers.a — a native dependency the earlier
change did not account for (it happened to be present on the dev machine). The
pure-Go default and gortex's own ONNX path do not need it.
Install the matching daulet/tokenizers v1.27.0 archive in the CI gomlx step
(mirroring the onnxruntime install), teach 'make build-gomlx' to fetch it
cross-platform, and document the requirement so a user building the XLA backend
knows to provide libtokenizers.a.
The gomlx build entry points passed only -tags embeddings_gomlx, but hugot gates
its real XLA session behind its own XLA build tag — so every gomlx build linked
the disabled stub, the GoMLX provider always errored at runtime, and the local
chain silently fell through to the pure-Go backend. The GoMLX backend was dead
code in every shipped binary.
Move the Makefile target and the docs build command to -tags "embeddings_gomlx
XLA" so the XLA session is actually compiled (verified to build with no system
XLA libraries — PJRT is a runtime download). CI now compiles both the tag pair
and embeddings_gomlx alone (a regression guard for scripts that still pass it by
itself). The docs are honest that XLA runtime viability is platform-dependent
and experimental, with the pure-Go backend as the reliable default.
The 0.52.2 cask aborted at load on macOS Sequoia/Tahoe with
"__DATA_CONST segment missing SG_READ_ONLY flag" (#176). darwin was
cross-compiled inside goreleaser-cross, whose osxcross ld64 omits the
SG_READ_ONLY flag on the __DATA_CONST segment that the macOS 15+ dyld now
enforces. Apple's ld sets it, so locally built binaries always worked and
the bug only surfaced in the released artifact.
Build darwin on a native macos-15 runner instead:
- new build-darwin job links arm64 natively + amd64 via `clang -arch
x86_64`, codesigns with rcodesign, smoke-tests the segment flag, and
runs `gortex version` on the enforcing dyld before handing off archives.
- the release job now builds linux only, merges the darwin tarballs,
notarizes, appends them to checksums.txt, signs, and uploads.
goreleaser's prebuilt builder is Pro-only, so the homebrew cask (which
references all four os/arch tarballs) is assembled and pushed to the tap
from the release job once every platform's hash exists, instead of by
goreleaser. Completions are still generated at brew-install time.
scripts/verify-macho-readonly.sh is the guard: it fails the release if a
darwin binary ships without SG_READ_ONLY, so this can never regress.
Register Oh My Pi as a new agent adapter writing config to `.omp/mcp.json`.
Includes detection in the init wizard, smoke test coverage, docs update,
and GCX default-format recognition for the `omp-coding-agent` client ID.
claude-plugin/ is not tracked in this repo (the bundle is published to a
separate repo on release), so a local 'make claude-plugin-check' git-diff
is a no-op — and 'make' builds with -tags llama, which would break a
stock CI runner. Drop that step; the all-platform render golden test and
the structural --check (both pure-Go, no llama) are the real PR fence.
Extend drift detection from the Claude bundle to every agent adapter.
A new render engine applies each adapter in an isolated sandbox (with
ForceDetect so it renders regardless of which tools are installed) and
serialises its generated MCP config, instructions, hooks, and routing
blocks into a normalized, machine-independent manifest.
- go test ./cmd/gortex -run TestAgentsRenderGolden byte-compares all
16 adapters against committed goldens (regenerate with
-update-agent-render); a stale or missing golden fails the test.
- gortex agents render --check is a structural CI gate (every adapter
still emits a gortex registration); --target dumps the manifests.
- A skill-drift GitHub Actions workflow runs both on every PR and
lifts the Claude-bundle fence (make claude-plugin-check) out of its
release-only trigger.
ForceDetect is a new ApplyOpts field honoured by all adapters; it is
never set during a normal gortex init.
Replace the floating go-version 1.26 pins in the ci, release, and security workflows with go-version-file: go.mod, so every job builds and scans with the exact patched toolchain go.mod declares (1.26.4) instead of whatever patch the setup-go manifest happens to serve.
Makes go.mod the single source of truth and keeps govulncheck from flagging stdlib advisories already fixed in a patch the floating manifest lagged behind. Matches the go-version-file pattern already used in bench-arm, init-smoke, and publish-claude-plugin.