Diagnose a bad answer or an empty index¶
Each section here starts from something you can see in the output and works back to the cause.
phpbotscout doctor is worth running first for the general environment check; everything below is
specific to this tool.
The binary will not start¶
The loader refuses it before phpbotscout runs any code, so there is no log line and no hint of its own. The system's C library is older than the runtime requirements allow:
phpbotscout: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.33' not found (required by phpbotscout)
phpbotscout: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.32' not found (required by phpbotscout)
phpbotscout: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.34' not found (required by phpbotscout)
That is Debian 11, Ubuntu 20.04 and anything of their age. No setting fixes it: the machine needs glibc 2.34 or newer.
libdave.so: cannot open shared object file means the binary is v0.5.0, the one release that
linked Discord's voice library. Upgrade rather than installing the library.
index sources says 0 of N qualify¶
Look at the CLAUSE column before anything else. It names which rule rejected each candidate.
A wall of unresolved with 403 Forbidden means the forge token cannot read Pages settings:
phpboyscout/go-tool-base rejected unresolved could not read the documentation site: GET https://gitlab.com/api/v4/projects/phpboyscout%2Fgo-tool-base/pages: 403 {message: 403 Forbidden}
Discovery needs a token with the Maintainer role on the group. Without one, or with a weaker
one, every documented repository is unresolved: "I could not find out", not "there is no site".
It is not a rejection on the merits, and retrying without fixing the token changes nothing.
Check the token is reaching the tool at all: gitlab.auth.env holds the name of an environment
variable, not the token, and resolution falls back to GITLAB_TOKEN when the section names
nothing.
A wall of unresolved reading no estate feed available this run, fetched or stored means the
estate feed could not be fetched and the index holds no earlier copy. Check that
index.feed.url is reachable from where the tool runs. Once one
fetch succeeds, the copy is stored, and later outages use it instead.
A feed rejection means the estate feed, projects.json, does not list the repository, or lists it
with listed: false. The feed is canon for what may be indexed, and the blog maintains it, so a
missing project is added there, not here.
no-docsite means the repository carries no zensical.toml and is outside
phpboyscout/go/, phpboyscout/rust/ and phpboyscout/images/, where a project with no docs site is
still admitted. That is a real verdict, not a configuration problem.
A much shorter list than you expected, with no unresolved rows, usually means the token is
being read but only sees public projects. The group enumerated is phpboyscout and it is compiled
in, so it is not something a config key can point elsewhere; gitlab.url.api only changes which
instance is asked.
Every question is declined¶
Start by separating retrieval from composition:
No model is called, so this costs nothing.
Nothing matched. Check there is an index at all:
If status shows content but --retrieval-only shows nothing, the query probably has no
searchable terms in it. Terms are split on anything that is not a letter or digit, so a question
made of punctuation and symbols produces an empty query and matches nothing.
The right passage is in the list, and the answer is still a decline. That is composition judging the passages insufficient. It is a legitimate outcome: the passages discuss the subject but do not answer the question. That question is a documentation gap, which is the most useful thing this tool produces.
You are in the wrong directory. index.path defaults to phpbotscout.db relative to the
working directory, and ask creates the file when it is missing rather than complaining. A brand
new empty index that declines everything looks exactly like a broken one. phpbotscout config get
index.path and pwd settle it in a second.
ask fails instead of answering¶
An error is not a decline. These are the ones you will actually hit.
ERRO ask: build claude client: unable to construct client: Anthropic API key is required but not provided
ai.provider: claude reads the key from ANTHROPIC_API_KEY and from nowhere else. There is no
ai.auth.env and no keychain support for it; see
the AI credential.
Only claude and claude-local are linked into the binary, so any other name, openai, gemini
and ollama included, fails like this. Use one of the two; see what phpbotscout does not
do.
ERRO answer: compose: answer: ask provider: claude subprocess failed: Error: --json-schema is not a valid JSON Schema: no schema with key or ref "https://json-schema.org/draft/2020-12/schema"
The default provider, claude-local, drives your locally installed claude binary, and that
binary rejected the structured-output schema because it named the 2020-12 meta-schema. Releases from
v0.6.1 send a schema the CLI accepts, so this means an older phpbotscout: upgrade it. Retrieval is
unaffected either way.
The directory in index.path does not exist. A missing file is created; a missing directory is
not.
A configuration change had no effect¶
Check which file is being read.
--config replaces the default paths rather than adding to them, and it also suppresses the
project-local .phpbotscout.yaml layer. If you passed --config, your project file is not being
read.
Check the precedence. An environment variable under the PHPBOTSCOUT prefix overrides the
file, and a changed CLI flag overrides that. PHPBOTSCOUT_AI_PROVIDER will quietly win over
ai.provider in your config.
Check it is not a restart-only key. While serve is running, only
discord.guilds[].exclude_channels applies live. A change to discord.auth, a server's
post_answers, or the list of servers logs a warning naming the field and keeps the old value:
Every other key is read once at startup and changes without a warning. See changing configuration while it runs.
Check the value parsed. An unrecognised log.level is ignored silently, and config get
log.level will read back the invalid value you set even though the logger never used it.
The bot is connected but seems to be seeing nothing¶
Read the startup line. It confirms the server and the excluded channels before a single message arrives:
The bot reads only channels that everyone in the server can view. A channel visible to a role, even one most members have, is not read, and neither is a private thread. If the channel you are testing in is restricted, that is the cause.
Then check readiness rather than liveness. Readiness tracks the gateway connection, so a reconnecting bot is live but not ready:
A bot that is up but disconnected looks exactly like a quiet channel, which is why the two endpoints answer different questions. See run the bot.
The index is stale and refresh keeps failing¶
A failed refresh is degradation, not an outage: the daemon keeps running, liveness passes, and readiness deliberately does not fail on a stale index.
index status records the reason against the source, so it survives the run that produced it:
Answering from a stale index with a caveat beats not answering, and restarting the daemon will not make an unreachable forge reachable.
If the log says index refresh stopped early with a rate-limit reason, the forge refused a fetch
and the run stopped rather than keep asking. index status marks the sources it did not reach
deferred, and the next tick tries them again. Nothing needs doing unless it happens every tick,
in which case set an SSH key, which takes the
fetches off the web rate limit, or lengthen index.refresh_interval.
Over SSH, a source failing with a knownhosts error means the forge's host key is not the one
pinned in index.ssh.known_hosts:
Do not add whatever key the forge now presents. Compare it with the fingerprints the forge publishes
first, since a changed host key is exactly what the file exists to catch. A source failing with
ssh: handshake failed: ssh: unable to authenticate means the forge does not accept the key in
index.ssh.key: check it is registered on the bot's account.