Answer your first question from the docs¶
By the end of this you'll have phpbotscout installed, a local index of every documented phpboyscout repository, and a real answer with a citation you can click. You'll also have seen it refuse to answer something, which is the behaviour worth understanding early.
Allow about twenty minutes. Most of that is two downloads: the Go dependency tree the first time you build, and the corpus itself, which is a shallow clone of fifty-odd repositories.
What you need first¶
Go 1.26 or newer. There is no released binary yet, so installing means building from source.
A GitLab personal access token with the Maintainer role on the phpboyscout group. This is the
step that bites, so it's worth being blunt about it: without one, you will get a corpus of nothing.
Discovery reads each repository's Pages settings to find its documentation site, and GitLab gates
that endpoint behind Maintainer. With a weaker token — or none — every candidate comes back
unresolved and zero sources qualify. The clone itself needs no token; discovery does.
A way to reach a model, for the last step only. Everything up to and including retrieval works
with no model at all. Composing an answer needs either an ANTHROPIC_API_KEY, or a locally
installed and authenticated claude binary.
Install phpbotscout¶
You need Linux, Go 1.26 or newer, and a C toolchain — the SQLite driver is the native one, under
cgo. From a checkout, just build is the shortest path. Otherwise:
The -tags sqlite_fts5 is not optional. Leave it off and the build succeeds, then every search in
this tutorial fails with no such module: fts5.
Nothing has been tagged yet, so @latest resolves to the current commit on the default branch —
you'll see a version string like v0.0.0-20260802151021-3eb67112ae53. That is expected, not a
warning sign.
Check it landed:
You'll see fourteen commands. Three of them — serve, ask and index — are phpbotscout's; the
rest come from the framework it's built on.
Create a configuration file¶
phpbotscout refuses to run without one:
ERRO failed to load configuration: no config file found hints="Run 'phpbotscout init' to create a configuration."
For a throwaway run, a project-local file is quickest. Make a working directory — the index lands in it — and drop a config beside it:
$ mkdir ~/phpbotscout-demo && cd ~/phpbotscout-demo
$ cat > .phpbotscout.yaml <<'YAML'
gitlab:
auth:
env: GITLAB_TOKEN
ai:
provider: claude
YAML
auth.env is the name of an environment variable, not the token. That's the whole point of it:
the secret never enters a file that might get committed.
Now export the token itself:
Where the config actually came from
phpbotscout config path prints the files being read. Worth running once, because --config
replaces the default paths rather than adding to them, and a project-local file is suppressed
when you pass it.
See what qualifies before indexing anything¶
Every candidate is listed, rejected ones included, with the clause that decided it:
SOURCE VERDICT CLAUSE REASON
phpboyscout/go/config indexed none public, in-group, documented
phpboyscout/go/config-toml rejected no-docsite no zensical.toml
phpboyscout/blog rejected visibility visibility is "private", not public
phpboyscout/infra rejected excluded permanently excluded by "phpboyscout/infra"
53 of 101 sources qualify.
If that last line says 0 of … qualify, look at the clause column. A wall of unresolved with
403 Forbidden in the reason means the token problem from the prerequisites — not a bug, and not
something a retry will fix.
Build the index¶
$ phpbotscout index build
53 sources: 53 indexed, 0 unchanged, 0 failed
1786 documents, 11928 chunks
That took a little over a minute here. Each qualifying repository is shallow-cloned into memory, never to disk, and only the files the corpus indexes are read at all.
The database lands at phpbotscout.db in the working directory. That default is relative, so
running these commands from a different directory silently works on a different index — a thing to
know before you wonder where your corpus went.
Check what you got¶
$ phpbotscout index status
SOURCE DOCS CHUNKS COMMIT AGE
phpboyscout/afmpeg 59 382 8f2d6078 1m
phpboyscout/cicd 57 410 1759234a 1m
phpboyscout/go-tool-base 435 3085 c688fc4b 1m
AGE is how long since that source last indexed successfully. status reads the local database
only, so it keeps working when the forge does not — which is exactly when you want it.
Look at retrieval on its own¶
Before spending anything on a model, see what the question actually finds:
$ phpbotscout ask --retrieval-only --limit 5 "what byte limit is used for oversized chunks"
21.38 package corpus › Chunk
https://gitlab.com/phpboyscout/phpbotscout/-/blob/3eb6711…/pkg/corpus/chunk.go#L35
18.59 package corpus › DefaultMaxChunkBytes
https://gitlab.com/phpboyscout/phpbotscout/-/blob/3eb6711…/pkg/corpus/chunk.go#L15
No model is called, so this costs nothing. Higher scores are better — the raw BM25 numbers are negative and get flipped once at the boundary so nobody downstream has to remember that.
This is the tool to reach for when an answer disappoints you. If the right passage isn't in this list, the problem is retrieval; if it's here and the answer is still wrong, the problem is composition.
Ask a real question¶
$ export ANTHROPIC_API_KEY='sk-ant-…'
$ phpbotscout ask "How do I stop a gtb tool checking for updates?"
To stop a gtb tool from checking for updates, set the `update.policy` configuration key to
`disabled`. This will log only without checking for updates.
Sources:
Configuration Keys › Self-update
https://go-tool-base-ff2fdf.gitlab.io/reference/config/#self-update
Follow the link. It goes to a real page, at the section the answer came from — every substantive claim has to be traceable to a passage that was actually retrieved, and a citation naming a passage outside that set gets the whole answer thrown away.
The default provider may not compose
Leave ai.provider unset and phpbotscout uses claude-local, which drives a locally installed
claude binary. Against Claude Code 2.1.220 that path fails with
--json-schema is not a valid JSON Schema, and you get an error rather than an answer.
Setting ai.provider: claude with an API key, as above, works. Retrieval is unaffected either
way.
Now watch it refuse¶
Ask something the documentation genuinely does not cover:
$ phpbotscout ask "What is the airspeed velocity of an unladen swallow?"
No answer from the indexed documentation.
nothing in the indexed documentation matched this question
This is the behaviour to get comfortable with. A decline is not a failure to try hard enough — it is a documentation gap with a timestamp on it, and the stream of them is meant to become a prioritised list of things to write. An error means something broke; a decline means nobody wrote it down.
Try one more, this time on a subject the corpus definitely covers but with a question it does not answer. You may well get a decline, and when you do it'll show you the closest passages it found, with scores — so you can tell "nothing matched" apart from "the right page was there and didn't say it".
Keep it current¶
build re-indexes everything. Day to day, use:
Run straight after a build and nothing has moved, so nothing is re-indexed. Only sources whose commit has changed get re-read. Every source is still fetched either way, so what you save is chunking and writing rather than network traffic — a refresh takes about as long as a build.
Reach for build again after anything that changes how content is chunked or ranked.
What to read next¶
- Run the bot — the daemon, its health endpoints, and what it reads versus what it keeps.
- Indexing reference — which files are indexed, where chunks are cut, and the numbers behind them.
- How an answer is produced — what happens between your question and the citation.
- What phpbotscout does not do — before you plan around a feature that isn't there.