Skip to content

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:

$ go install -tags sqlite_fts5 gitlab.com/phpboyscout/phpbotscout/cmd/phpbotscout@latest

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:

$ phpbotscout --help

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:

$ export GITLAB_TOKEN='glpat-…'

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

$ phpbotscout index sources

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:

$ phpbotscout index refresh
53 sources: 0 indexed, 53 unchanged, 0 failed
0 documents, 0 chunks

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.