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.
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.
Everything no-docsite means the repositories carry no zensical.toml. 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.
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.
openai and gemini cannot be used. Neither implements the one-shot contract ask requires. Use
claude or claude-local; see what phpbotscout does not
do.
That provider name is not registered. Only claude, claude-local, openai, gemini and
openai-compatible exist, and only the first two work.
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 is rejecting the structured-output schema phpbotscout sends. Reproduced against Claude Code
2.1.220. Retrieval is unaffected; set ai.provider: claude with an API key to compose.
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[].channels applies live. Everything else logs a warning naming the field and keeps
the old value:
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 allowlist before a single message arrives:
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.