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.

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:

$ 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: 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: provider openai does not support Config.Stateless

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.

ERRO ask: build ollama client: unsupported provider: ollama

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.

ERRO ask: open index: store: open: unable to open database file (14)

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[].channels applies live. Everything else logs a warning naming the field and keeps the old value:

WARN configuration change requires a restart field=auth

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:

INFO watching channels space=1531227937678954747 channels=[1531227938622800055]

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.