dazuma
Blocked by: #2, #3 ## Summary Add a Toys tool `agentdocs lookup` — a **Reader**, in `CONTEXT.md`'s sense: an executable that answers an agent's lookup against a bundle and returns the one member or concept asked for. Today `skills/yard-agentdocs/SKILL.md` coaches an agent through that procedure in prose: resolve the version, compute the XDG path, derive the file path from the FQN, grep for the heading. Roughly 70% of the skill is format mechanics restated as prose, which is the duplication the three-owner anti-drift rule exists to prevent — a prose file cannot *execute* `bundle.md`'s mechanics, only paraphrase them. ## Why Primary objective is **correctness**, not token economy. The skill's highest-stakes lines — "never read a different version's tree", "never `--all`", "never read `index.md`" — are currently unenforceable prose. In code they are invariants. Secondary objective is round trips: a lookup today costs 3–4 inference turns (grep `Gemfile.lock`, probe the path, maybe build, grep the file) and becomes one. ## Interface ``` agentdocs lookup <gem> <entity> [--version VERSION] [--full] [--no-build] ``` - `<gem>` — required. The disambiguator; the agent essentially always knows it. - `<entity>` — an FQN, optionally with a member: `Toys::Acceptor::Enum` or `Toys::Acceptor::Enum#initialize`. Ruby operator method names collide with shell metacharacters (`#[]`, `#<<`, `#*`, `#=~`), so `long_desc` must state that the argument should be quoted. - `--version` — override. By default the version is resolved from the project's `Gemfile.lock` (four-space match on the resolved spec, read **as a file** — the tool runs outside `bundle exec` deliberately, so it must never go through Bundler's runtime), falling back to the newest installed version. Version resolution is the single most mechanical and most error-prone step in the current skill; leaving it to the caller would preserve the exact failure mode this tool exists to remove. ## Output **Member target** (`Foo::Bar#baz`): that one `### ` section, plus the file's identifying context lines (`**Superclass:**`, `**Includes:**`, `**Extends:**`), so a method section is never presented without saying what it belongs to. **Concept target** (`Foo::Bar`): everything from the start of the file through the end of the `## Member Summary` section — that is, up to but not including the first `## ` heading that *follows* `## Member Summary`. If there is no `## Member Summary`, print the whole file. Precise rule matters: across 4,000 sampled concept files in a 117-bundle local corpus, 316 have no H2 at all (tiny error subclasses — printing whole is correct), and 5 have an H2 *before* `## Member Summary` because the gem's own docstring prose contains `##` headings (`fileutils`, `bundler/TSort`, `psych`, `did_you_mean`). "First H2" is therefore the wrong delimiter; "first H2 after Member Summary" is right in all cases. An agent querying a concept without a member is orienting — asking what a module is for, or hunting for the right member — and the full body costs tokens for detail that is not relevant at that stage. `--full` prints the whole file. The output shape is deterministic regardless of file size: no byte threshold, no truncation marker. **Provenance header**, on every success: gem name, resolved version, concept path within the bundle, and the gem root. The version invariant is otherwise enforced but invisible — an agent receives markdown with nothing naming where it came from. The gem root is free here (the tool has resolved the spec) and removes the `bundle show` hop the skill currently documents, because `**Defined in:**` paths are gem-root-relative and nothing in a bundle records the root. The body is reproduced **verbatim**. Do not rewrite `**Defined in:**` lines to absolute paths: that would make the Reader a transformer rather than a reader, and `bundle.md` would stop describing what the agent actually sees. ## Behavior **Building.** Builds a missing bundle inline (on by default; `--no-build` to refuse), composing `GemBuilder` — which already exposes `output_dir_for`, `resolve`, `build`, `rebuild:` and a configurable `spec_dirs`. Progress goes to stderr so stdout stays pure output. Depends on #3 for atomicity: a build killed by an agent's command timeout must not leave a partial bundle behind. **Never installs the subject gem.** Installing arbitrary third-party code permanently, on an agent's behalf, for a documentation lookup, is out of proportion — and the case that seems to motivate it mostly is not real, since a gem in `Gemfile.lock` is nearly always already installed. Installing `yard-agentdocs`/`toys` themselves is already handled by `toys do --gem=yard-agentdocs --on-missing-gem=install` and `gem install toys`. **Vendored bundle paths.** When a project uses `bundle config path vendor/bundle`, the gem *is* installed but outside `GemBuilder.default_spec_dirs`. Point `spec_dirs` at the project's bundle path rather than installing a second global copy. This is the real gap that "auto-install" was reaching for. **Default gems.** Build them. Depends on #2, which makes an explicitly named default gem buildable. **Git-, path-, and vendored-*source* dependencies.** Detect, report the resolved source path, exit 3, never build and never invent a location. A location convention for non-release sources is a standing open question in `docs/dev/DESIGN.md` ("Bundles for dependencies that aren't installed releases"); this tool must not preempt it. Detecting and reporting these turns prose exclusions the agent has to *recognise* into an accurate runtime answer. ## Exit codes and the not-found contract An LLM caller reads stdout, not `$?`, so the **body is primary** and must carry the agent's next move in every failure case — never a bare "not found". Codes exist for scripting and to keep the cases distinct: - `0` success - `1` not found — either the entity has no matching `### ` heading in an existing concept, or the FQN has no file. List near-miss headings from that concept, and matching headings elsewhere in the bundle, **explicitly labelled as candidates**. - `2` usage error (Toys' own convention) - `3` no bundle possible for this dependency kind — report the source path - `4` build failed Exact lookup **never silently falls back to fuzzy matching**. A guess that reads as an answer is the plausible-wrong-answer failure this whole tool exists to eliminate. ## Not in scope - Fuzzy/discovery lookup — deferred to a separate `agentdocs search` subtool (see the follow-up issue). Consequence: the two discovery grep recipes stay in `SKILL.md` for now. - Locking between concurrent builders (see #3). - Corpus-wide structural invariant checks (see the follow-up issue). - Any change to `bundle.md`'s preamble. It is in-band and version-locked to a tree that may be read where this gem is not installed; mentioning the Reader would couple a generated artifact to out-of-band tool availability and break the accelerator-not-gateway property below. ## Architecture Behavior in `lib/yard/agentdocs/`, tool file holds only the Toys DSL and prompting — matching `Builder`, `GemBuilder`, `GemCleaner`, `SkillInstaller`. The Reader **implements** `bundle.md`'s mechanics rather than restating them, so the preamble stays authoritative and the two cannot disagree. It is an **accelerator, never a gateway**: reading a bundle directly with grep remains a first-class path, bundles stay portable to harnesses with no Ruby runtime, and the format — not the Reader — is the contract. A Reader bug degrades to "grep still works." ## Testing Unit tests over `examples/geometry/doc`, the existing trusted in-repo fixture. That is the CI-gating layer, and the only layer in scope here. ## Documentation obligations - `long_desc` on the tool — authoritative user documentation, including the argument-quoting note. - `SKILL.md` rewritten: the mechanics it currently restates move to the tool. Residue is the trigger `description`, dependencies-only scoping, the one `lookup` invocation, the stranded discovery greps, prefer-local-source-over-web, and a much-reduced give-up rule. Expect ~35 lines, down from ~65 — not the ~18 that deferring `search` would have bought. - `CONTEXT.md` — already updated: **Reader** added under "Surfaces that consume the format", **Skill** narrowed to judgment. - **`docs/dev/Tooling.md`**: a dated section when this lands, carrying the alternatives this design considered and rejected, so they are not re-proposed: installing the subject gem; embedding the script in `SKILL.md` (rejected — `install-skill` copies the directory and freezes it, so a stale copy would misread newer bundles silently); byte-threshold truncation of concept output; fuzzy fallback on an exact miss; rewriting `**Defined in:**` to absolute paths; build locking; and standing up `docs/adr/` for this (rejected on the same grounds DESIGN.md rejected it on 2026-09-12 — the repo already has a decision log, and a second one is a drift surface). Design settled in a grilling session, 2026-09-15. No `docs/adr/` entry; this document plus the `Tooling.md` section are the record.