hooks/cli: never render bare tool verbs; teach and recover the gortex call shape#260
Merged
Conversation
Hook and adapter guidance named MCP tools by bare name with no invocation shape, so an agent in a shell context (MCP unmounted or degraded, or one that chose Bash) invented the invalid `gortex read_file <path>` verb. There is no such top-level verb; the correct fallback is `gortex call <tool> --arg k=v`. Add internal/toolref, one shared renderer for tool references: MCPRef for MCP-directed guidance and CLIFallback/FallbackLine for the shell fallback, with realistic per-tool example args. Wire it into every runtime guidance emitter — the Read/Grep/Bash/Glob/Edit/Write deny and soft-guidance messages, the adaptive-nudge reminder, the subagent briefing (Claude + Kimi), the whole-file read advisory, the session-start rule preamble, and the prompt-relevant symbol injection. Add internal/mcp.RegisteredToolNames, a daemon-free enumeration of the real tool registry, and a regression test that iterates it against every emitted guidance template and fails on any bare `gortex <tool>` shape.
The slimmed policy core no longer told agents how to reach an MCP tool from a shell, so the only shape they had was the bare tool name — which they turned into the invalid `gortex read_file <path>`. Add one line to both the global and per-project instruction bodies: every tool is reachable as `gortex call <tool> --arg k=v`, and there is no bare `gortex <tool>` verb. The global section stays well within its byte ceiling.
Running a bare MCP tool name as a top-level verb (`gortex read_file foo.go`) only got cobra's opaque "unknown command". Before cobra errors, detect when the first positional argument names a registered MCP tool that is not a real subcommand and print a did-you-mean pointing at `gortex call <tool> --arg …` with a realistic example arg, then exit nonzero. The check is daemon-free — it matches against the in-process tool registry — and only runs after the cheap flag/known-verb rejects, so a normal invocation pays nothing.
The Hermes and Kiro adapters render their own tool-swap tables instead of the shared instructions body, so they missed the restored CLI-fallback rule: an agent reading those docs from a shell context still had only bare tool names and would invent the invalid `gortex <tool>` verb. Add the same one-line rule to both docs — every listed tool is reachable as `gortex call <tool> --arg k=v`, with a worked read_file example.
Agents don't only run exact tool names as verbs — they invent truncations like `gortex index` and `gortex reindex`, which the exact-match recovery missed and cobra answered with a bare "unknown command". Extend the interceptor with a conservative fuzzy pass over the tool registry: an unknown verb that is an underscore-boundary prefix of a tool name (reindex → reindex_repository) or an exact underscore-delimited token of one (usages → find_usages) draws up to two `gortex call <tool> --arg …` suggestions. Verbs shorter than four characters, real CLI verbs, and flags never match, so cobra's own error remains the fallback for genuinely unknown commands.
Every symbol-ID example rendered `<file>::<Name>`, so agents targeting a method issued the bare-name form and got "symbol not found" — a method ID carries the receiver (`pkg/s.go::Server.Handle`). Render both forms (`<file>::<Name|Recv.Name>`) in the shared toolref example args, which propagates to every hook guidance line and the CLI did-you-mean, and spell the receiver form out in the two docs that hand-render a get_symbol_source ID example. Byte ceilings unaffected.
… docs The Hermes and Kiro policy docs gained the shell-fallback rule and the method-form symbol-ID example, but the all-platform render goldens were not regenerated, so the byte-compare gate failed. Regenerated via the golden test's -update-agent-render flag; the diff is exactly the two intended doc lines.
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.
Fixes #259.
What was happening
Hook and adapter guidance named MCP tools (`read_file`, `get_symbol_source`, ...) without ever showing a callable shell shape, and the generated CLAUDE.md no longer carried the `gortex call ` fallback rule. An agent operating from a shell (MCP unmounted or degraded) would invent `gortex read_file ` — a nonexistent top-level verb — and hit `unknown command`.
Reproduced under a controlled harness (hooks active, MCP not mounted, 15 sessions): invalid invocations in 13% of sessions (`gortex index status`, `gortex reindex ...`), 10/15 sessions dodging the deny via `git show`/`sed`/`grep`, and `gortex call` discovered only by `--help` trial-and-error — never from the hook text.
The fix (three layers + two transcript-driven refinements)
Validated end to end
Same 30-session harness against this branch, gates enforced: invalid invocations 0/30 sessions; degraded-condition sessions answering via gortex 15/15 (from 5/15; zero deny-dodges); `gortex call` shape used correctly first-try in 15/15 (deny fired only once — the policy-core line pre-empts the path); 15/15 recovery on the remaining symbol-naming misses via the tool-level did-you-mean.
`go test -race` on all touched packages, golangci-lint, skill-drift, and the ambient byte-ceiling tests all green (global section 4,505 B of 6,656; skills 2,147 B of 2,560).