fix: print the one instruction in a form that runs in the state it describes (iss-207) - #229
Merged
Merged
Conversation
…scribes
hooks/bootstrap.sh's success notice and README.md both said to run
`abcd ahoy install` once. The whole premise of that sentence is that `abcd` is
not a name the shell can resolve — putting it there is what the command does —
so the instruction failed with "command not found" for precisely the reader it
was written for.
On the first manual plugin install the consequence was not cosmetic: the agent
reading the notice could not run the printed command, invented
`cd <plugin cache> && go run ./cmd/abcd ahoy install`, and told the user to run
that instead — a source-build path needing a Go toolchain, reaching into the
harness's plugin cache, and not the documented install at all.
The script already holds the absolute path as $binary, so the notice prints it:
the instruction becomes `"<absolute path>" ahoy install`, resolvable right now,
by anyone, with no toolchain. The invocation is last on the line so it stays
copy-pasteable, and the provenance parenthetical moves ahead of it rather than
trailing the command. A2's contract is preserved — the success still leads the
first word of the first stderr line, which is all the transcript renders.
README.md carries the same invocation with the one part a committed file cannot
know left as a placeholder the notice fills in. `${CLAUDE_PLUGIN_ROOT}` is
deliberately NOT used there: it is bound for a hook and unset in the terminal
the sentence is addressed to, which would reproduce the same defect in a new
shape.
CI holds both surfaces: every occurrence of `ahoy install` the notice or the
README prints must be reached through a path rather than a bare name — an
every-occurrence check, because a text can carry the runnable form and still
leave the unrunnable one standing beside it. Reading it on a real plugin cache
stays the manual install gate; the plugin root and the harness are not present
in CI.
Resolves iss-207.
Assisted-by: Claude:claude-opus-5[1m]
…proof Three review findings on the iss-207 change. The README defined `<plugin-root>` solely by reference to the bootstrap's success notice — but that notice prints once per plugin root, because every later session takes the fast path, and nothing else in README.md or docs/ names the plugin root or its location. The reader who still has the notice on screen already has the whole command; the reader who does not could not instantiate the placeholder at all. It also pointed at the wrong thing: the notice prints the BINARY path, not the directory. The paragraph now says what `<plugin-root>` is in host-agnostic terms — the directory the agent harness unpacked the plugin into, binary directly inside as `abcd` — notes that copying the command out of the notice is the short route, and sends a reader who cannot instantiate it to the install one-liner, which needs no plugin root. Naming the conventional cache path is not available here: docs-lint's harness/* rules block naming a specific harness in user-facing prose, which is the right trade. The same insertion had left a ragged re-wrap, a 13-character orphan line between two full-width ones. The paragraph is re-flowed and split at the change of subject. The printed command wrapped the path in DOUBLE quotes, which survives a space or an apostrophe and then leaves `$`, a backtick and a `"` live — a plugin root holding one of those would expand, substitute, or terminate the string on paste, on the one line whose entire job is to be pasted. It now uses the single-quoted form the repo already owns for exactly this (internal/core/ahoy's shSingleQuote, mirrored in POSIX sh), rewriting each embedded `'` as `'\''`. The README shows the same shape, so the two surfaces do not hand the reader two different spellings of one command. The new case does not compare strings: it installs into a plugin root whose name carries an apostrophe, a $HOME, double quotes and a backtick pair, then hands the command the notice printed to a real `sh` and requires it to run the binary at that path. Watched fail against the double-quoted form, where the pasted command expanded $HOME and executed the backticks. The one-line shape and A2's success-leads-the-first-line contract are unchanged and still asserted. Assisted-by: Claude:claude-opus-5[1m]
…instruction Assisted-by: Claude:claude-fable-5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Item A3 of the install-experience run (
.abcd/development/plans/2026-08-11-install-experience.md§A3), fixing iss-207.This branch is cut from
fix/iss-204-sessionstart-collapse(A2, head90d8339), because A2 and A3 rewrite the same sentence inhooks/bootstrap.sh. A2 made the success lead the first visible stderr line; A3 replaces that sentence's content with a runnable form. Branching off main would have guaranteed a conflict on one line.The base of this PR is
main, so A2's commit appears in the diff below. It is not part of this review.That is the A3 delta: one commit,
c619576, touching five files.The defect
hooks/bootstrap.sh's success notice andREADME.mdboth said to runabcd ahoy installonce. The whole premise of that sentence is thatabcdis not a name the shell can resolve — putting it there is what the command does — so the instruction failed withcommand not foundfor precisely the reader it was written for.On the first manual plugin install (2026-08-10) the consequence was not cosmetic. The agent reading the notice could not run the printed command, invented
cd <plugin cache> && go run ./cmd/abcd ahoy install, and told the user to run that instead: a source-build path needing a Go toolchain, reaching into the harness's plugin cache, and not the documented install at all.The fix
The script already holds the absolute path as
$binary, so the notice prints it. The new notice, in full:Three things about its shape are deliberate:
.binary-metaprovenance parenthetical moves ahead of it rather than trailing the command.internal/core/ahoy'sshSingleQuotealready owns. Double quotes survive a space or an apostrophe and then leave$, a backtick and a"live — on the one line whose entire job is to be pasted.abcd.README.mdcarries the same invocation with the one part a committed file cannot know left as a placeholder the notice fills in:${CLAUDE_PLUGIN_ROOT}is deliberately not used in the README here, even though A1 establishes it as the ladder shape for command surfaces. It is bound for a hook and unset in the terminal this sentence is addressed to, so it would reproduce the same defect in a new shape. The ladder is right where a hook runs the command; a printed path is right where a human does.Red → green
TestBootstrapPrintsARunnableInstruction(internal/surface/cli/bootstrap_test.go), two subtests, written first and watched fail against the unmodified surfaces:Green after the change. The notice half runs the real script against the existing TLS fixture harness and asserts on the actual bytes it prints, not on a stand-in.
The review round added a third subtest that does not compare strings at all: it installs into a plugin root whose name carries an apostrophe, a
$HOME, double quotes and a backtick pair, then hands the command the notice printed to a realshand requires it to run the binary at that path. Watched fail against the double-quoted form, where the pasted command expanded$HOMEand executed the backticks.The check is every-occurrence, not "contains the good form": it splits on
ahoy installand requires each occurrence to be reached through a path. A text can carry the runnable instruction and still leave the unrunnable one standing beside it, and only an every-occurrence check sees the second one.What CI does not prove
Per plan §4, the end-to-end reading of this stays the manual install gate. CI has no plugin cache and no harness, so what is proven here is that the printed bytes name an absolute path and that no bare-name instruction survives on either surface. That the path is the right one on a real install — and that the sentence now actually works when an agent follows it — is §4's job. Please do not read the green tick as covering that.
Notes
yes | abcd ahoy install(the non-interactive form fix: answer install prompts without a terminal, and say what --yes leaves (iss-167, iss-166) #226/A5 documents) is not printed here. This notice addresses a human at their own terminal, where the interactive path is fine, and adding a pipe would put a second thing on a line whose value is being copy-pasteable. No conflict with A5 either way.abcd capture resolve iss-207 --impact fixis in this PR; the issue moves open → resolved. No record link needed repointing — the plan names iss-207 as plain text, not as a link.[Unreleased] / Fixed, no reflow of neighbours. It sits directly above A2's entry, which is in this diff via the base — expect a conflict there and resolve it maintainer-side.Gates
make preflightexit 0 andgofmt -l .empty, both asserted at1ea7ecd02c898ad8f7114dc09418e5f5cf48091fimmediately before and after the run, working tree clean, branch unchanged either side.Review round
Adversarial review returned APPROVE, tracing the runnable-when-pasted question through
resolvePluginRoot. Its three findings are fixed in1ea7ecd, a plain commit on top (no rebase — the base branch is taking its own fixes):<plugin-root>was not instantiable. It was defined only by reference to the success notice, which prints once per plugin root; nothing else inREADME.mdordocs/names the plugin root, and the notice prints the binary path rather than the directory. The paragraph now says what the placeholder is in host-agnostic terms and sends a reader who cannot instantiate it to the install one-liner, which needs no plugin root. Naming the conventional cache path is not available here — docs-lint'sharness/*rules block naming a specific harness in user-facing prose, and that trade is the right one.sedline mirroringshSingleQuote, plus the test above. The one-line shape and A2's success-leads-the-first-line contract are unchanged and still asserted.Assisted-by: Claude:claude-opus-5[1m]