Skip to content

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:

$ phpbotscout ask --retrieval-only --limit 20 "your question"

No model is called, so this costs nothing.

Nothing matched. Check there is an index at all:

$ phpbotscout index status
Nothing indexed yet. Run `phpbotscout index build`.

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.

ERRO ask: build openai client: unable to construct client: unsupported provider: openai

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.

ERRO ask: open index: store: open: unable to open database file: no such file or directory

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.

$ phpbotscout config path

--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:

WARN configuration change requires a restart field=auth

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:

INFO reading public channels space=1531227937678954747 excluded=[1531227938622800099]

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:

$ curl -s localhost:8081/healthz | jq

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:

phpboyscout/go/controls last failed: clone refused

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:

knownhosts: key mismatch
knownhosts: key is unknown

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.