Skip to content

fix: print the one instruction in a form that runs in the state it describes (iss-207) - #229

Merged
REPPL merged 3 commits into
mainfrom
fix/iss-207-runnable-instruction
Aug 12, 2026
Merged

fix: print the one instruction in a form that runs in the state it describes (iss-207)#229
REPPL merged 3 commits into
mainfrom
fix/iss-207-runnable-instruction

Conversation

@REPPL

@REPPL REPPL commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Item A3 of the install-experience run (.abcd/development/plans/2026-08-11-install-experience.md §A3), fixing iss-207.

⚠️ Read only the A3 delta — this PR is branched off A2, not off main

This branch is cut from fix/iss-204-sessionstart-collapse (A2, head 90d8339), because A2 and A3 rewrite the same sentence in hooks/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.

git diff fix/iss-204-sessionstart-collapse...fix/iss-207-runnable-instruction

That is the A3 delta: one commit, c619576, touching five files.

The defect

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 (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:

abcd bootstrap: installed the checksum-verified abcd binary (release v0.4.2) into the plugin root, so the abcd hooks are live for this session. For the abcd command in your own terminal, run this once — the path is absolute because abcd is not on your PATH yet, which is exactly what the command fixes: '/…/plugin-root/abcd' ahoy install

Three things about its shape are deliberate:

  • The invocation is last on the line, so it is copy-pasteable to the end. The .binary-meta provenance parenthetical moves ahead of it rather than trailing the command.
  • The path is single-quoted, the form internal/core/ahoy's shSingleQuote already 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.
  • The success still leads the first word. A2's contract is preserved and re-asserted in this PR's own test — only the first line of a hook's stderr reaches the transcript.
  • The sentence says why the path is absolute, so a reader (human or agent) does not "helpfully" shorten it back to abcd.

README.md carries the same invocation with the one part a committed file cannot know left as a placeholder the notice fills in:

…put the plugin-root binary on your PATH by running it once by its absolute path — '<plugin-root>/abcd' ahoy install. […] <plugin-root> is the directory the agent harness unpacked the abcd plugin into, with the binary sitting directly inside it as abcd; the bootstrap's success notice prints that full binary path […]. That notice appears once per plugin root […] so if it has scrolled away […] the install one-liner above needs no plugin root at all.

${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:

--- FAIL: TestBootstrapPrintsARunnableInstruction/the_notice_names_the_absolute_binary
    the notice must print the absolute plugin-root binary the script already holds …
      output "…For the abcd command in your own terminal, run `abcd ahoy install` once.\n"
    the notice still prints an `ahoy install` a reader cannot run — `abcd` is not on
      PATH, which is the very state this sentence addresses
--- FAIL: TestBootstrapPrintsARunnableInstruction/the_README_carries_the_same_concrete_form
    README.md must give the one-time PATH setup as "\"<plugin-root>/abcd\" ahoy install"
    README.md still instructs an `ahoy install` that cannot be run before `abcd` is on PATH

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 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 check is every-occurrence, not "contains the good form": it splits on ahoy install and 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.
  • Ledger: abcd capture resolve iss-207 --impact fix is 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.
  • CHANGELOG: entry at the top of [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 preflight exit 0 and gofmt -l . empty, both asserted at 1ea7ecd02c898ad8f7114dc09418e5f5cf48091f immediately 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 in 1ea7ecd, a plain commit on top (no rebase — the base branch is taking its own fixes):

  1. MINOR — <plugin-root> was not instantiable. It was defined only by reference to the success notice, which prints once per plugin root; nothing else in README.md or docs/ 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's harness/* rules block naming a specific harness in user-facing prose, and that trade is the right one.
  2. NIT — ragged re-wrap. The paragraph is re-flowed and split at the change of subject.
  3. NIT — double quoting. Applied, and it stayed minimal: one sed line mirroring shSingleQuote, 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]

REPPL added 3 commits August 12, 2026 12:29
…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
@REPPL
REPPL enabled auto-merge August 12, 2026 14:26
@REPPL
REPPL merged commit 13a3f94 into main Aug 12, 2026
13 checks passed
@REPPL
REPPL deleted the fix/iss-207-runnable-instruction branch August 12, 2026 14:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant