Skip to content

feat: detect host tools for the onboard recipe, and rename info's tools key to harnesses - #394

Merged
thecodedrift merged 9 commits into
mainfrom
feat/onboarding-tool-detection
Sep 23, 2026
Merged

thecodedrift merged 9 commits into
mainfrom
feat/onboarding-tool-detection

Conversation

@thecodedrift

Copy link
Copy Markdown
Member

The onboard recipe assumed the GitHub CLI in prose, told the agent to probe for
it, named one issue tracker as the expected one, and offered PR-comment mining in
repositories that have no pull requests. It now reports what the CLI found.

The consumer-visible part: info --json renames tools to harnesses

tools has always carried agent harnesses — Claude Code, Codex, Cursor,
OpenCode — with each one's installed skills and their staleness. That is the
wrong noun for it, and it is the name this change needs. The array is unchanged
in shape; only the key moves:

-  .tools[].skills[]     # Claude Code, Codex, Cursor, OpenCode
+  .harnesses[].skills[]

tools then takes on what the word says: the command-line binaries found on
PATH, as { name, present, applicable, path? } for gh, git and jq.

Every consumer inside this repository moves with it, and they are all here:
src/schemas/info.ts, src/commands/info.ts (JSON and human output),
src/agent/info.md's example payload, and test/cli.test.ts. Nothing else in
the repo reads the key — no reference.json field, no recipe, no telemetry
payload, and there is no dashboard or generator package in this tree.

Detection is presence, and only presence

src/detect/host-tools.ts asks findOnPath whether a file of each name sits on
PATH. It existsSynces and executes nothing: no spawn, no --version, no
hash. The recipe is worded to match — "gh is on your PATH; Taskless did not run
it" — because "gh is available" would be a claim about a working install that
nobody checked.

Verification by hash was considered and rejected on measurement, not on
principle.
GitHub publishes sha256 digests for gh's release archives and
installers
, not for the extracted binary, and the Homebrew-installed gh 2.97.0
on the development host matched 0 of the 21 official digests. A tier that
reports "unverified" for the ordinary macOS install path is worse than no tier,
because a reader takes that word to mean "suspicious" rather than "not
checkable".

applicable is a second axis, and it outranks absent

gh in a repository with no GitHub origin is inapplicable however well it is
installed: there are no pull requests to read. That comes from
resolveRepositoryContext, the same resolution behind info and telemetry,
rather than from a new is-this-GitHub probe that could disagree with it.

Precedence matters because collapsing the two produces the one instruction that
cannot help: a GitLab user with gh installed being told to install gh. A
non-GitHub repository is told there are no pull requests to mine, and is not
offered the source even with gh present.

An omitted source always says why in one line, mirroring the reasoning
already written into route.md for the remote tier it declines to offer: a
reader who is not told reads the omission as an oversight and asks for it, which
costs a turn and arrives back where the recipe already is.

The mechanism is general; onboard is its only consumer here

RecipeOptions.hostTools carries the state into the renderer, which substitutes
whole passages — bullet marker, step title and grammatical connective
included — exactly as DETECT_EVIDENCE and LOGIN_EVIDENCE already do, and
never post-strips rendered text. With no hostTools supplied every passage
renders its default, so @taskless/cli/prompts gets the full menu unchanged: a
Worker consumer has no PATH worth describing, and a recipe trimmed against
this host's tooling would be describing the wrong machine.

Detection runs at the point invocation is already detected, in both serving
paths. agent runs it only for topics whose template actually contains one of
these variables, asked of getRawRecipe(...).variables (sprintf's own parse)
rather than of a hardcoded topic list, so unrelated topics pay no git
subprocess and the next recipe to adopt the mechanism needs no edit there.

Also in here

  • The issue-tracker bullet stops naming Linear as the expected tracker. Jira and
    Linear are named as examples of the class; whether one is reachable depends on
    the agent's MCP roster, which the CLI cannot see, so it stays the agent's
    judgement.
  • openspec/specs/cli-onboard/spec.md still said onboard.txt throughout. The
    file has been onboard.md since the Vale-coverage change, and the requirement
    whose title named the old path is a REMOVED plus an ADDED rather than a
    MODIFIED, because a MODIFIED block is matched by title and a renamed one
    applies nothing at all.
  • Topic onboard v3 → v4.

Verification

The spec delta was checked the way CLAUDE.md prescribes — archive against a
throwaway commit, diff the requirement and scenario lists, reset — because
openspec validate --strict passes on a delta that silently drops scenarios:

capability scenarios before after lost
cli 73 76 none
cli-onboard 27 31 none
cli-agent 39 41 none
cli-knowledge-prompts 30 34 none

Gates: pnpm typecheck clean, pnpm lint clean (pnpm cli check reports no
issues), pnpm test 1713 passed across 103 files.

Changeset is patch: the package is 0.y.z, where semver puts added surface
outside the stability guarantee. The rename is what the release note leads with.

Fixes #393

@thecodedrift

Copy link
Copy Markdown
Member Author

@claude /review

@github-actions github-actions Bot added the Open OpenSpec Contains unresolved OpenSpec changes. All openspec changes must eventually reach an archive state. label Sep 23, 2026
@claude

claude Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Claude finished @thecodedrift's task in 5m 52s —— View job


Review mode: incremental — read 0 prior review thread(s) before reviewing.

.prior-review.json contains no reviewThreads and no reviews — only the top-level @claude /review comment that triggered this run. There is nothing previously raised to classify as addressed or still-open, so this is effectively the first review of this PR, and everything below is [New].

  • Read .prior-review.json (0 prior threads/reviews)
  • Fetch PR diff (gh pr diff 394) and metadata (gh pr view)
  • Read the full changed files for context (host-tools.ts, recipes.ts, agent.ts, info.ts/schemas/info.ts, onboard.ts, git-remote.ts, platform-binary.ts, tests, openspec deltas)
  • Verify the spec deltas restate full requirements (spot-checked cli-onboard/cli-agent: scenario counts match the PR's own before/after table, nothing dropped)
  • Post findings as inline comments

Findings

1. packages/cli/src/detect/host-tools.ts:49 — gh/git/jq detection is silently broken on Windows.
detectHostTools calls findOnPath(name) with the bare command name. Every other caller of findOnPath in the codebase (rules/platform-binary.ts:243) wraps the name in executableName() first, which appends .exe on process.platform === "win32". Without that, existsSync never matches gh.exe/git.exe/jq.exe, so a Windows user with these tools installed is told present: false and the onboard recipe tells them to install a tool they already have. CI only runs ubuntu-latest, so nothing catches this. Left as an inline comment with the fix (reuse executableName).

2. packages/cli/src/prompts/recipes.ts:369 — the "not applicable" tool line hardcodes a GitHub-specific reason for any tool.
toolLine prints "this repository has no GitHub origin" for any HostTool with applicable: false, not just gh. Harmless today (only gh can be inapplicable), but this is a public render path over an arbitrary caller-supplied array, and nothing scopes the message to gh by name. Low severity — flagged as a robustness note, left as an inline comment.

Not flagged (checked, no issue found)

  • The tools → harnesses rename in info --json/schemas/info.ts/agent/info.md/test/cli.test.ts is consistent everywhere; no missed consumer in the repo.
  • applicable precedence over present (the core design point of the PR) is implemented and tested correctly, including the interesting case where PATH is empty and gh still reports not-applicable rather than absent (test/host-tools.test.ts, test/onboard.test.ts).
  • The agent/onboard byte-parity requirement is exercised across all three host-tool states in test/onboard.test.ts, including the empty-PATH case.
  • Spot-checked the OpenSpec delta files against CLAUDE.md's "a MODIFIED block replaces the whole requirement" warning: cli-onboard's MODIFIED requirement carries all 8 pre-existing scenarios plus the 3 new ones (11 total), and the REMOVED+ADDED pair for the renamed onboard.txt→onboard.md requirement is used correctly (title changed, so REMOVED+ADDED rather than MODIFIED, matching the guidance). cli-agent's MODIFIED requirement likewise carries its 7 prior scenarios plus 2 new ones. No scenario loss found in the ones checked.
  • The changeset bump (patch) is correctly justified: the package is 0.y.z.

Comment thread packages/cli/src/detect/host-tools.ts Outdated
Comment thread packages/cli/src/prompts/recipes.ts Outdated
@thecodedrift

Copy link
Copy Markdown
Member Author

Re: @claude[bot] — "Claude finished @thecodedrift's task in 5m 52s …"
#394 (comment)

Finding 1 (Windows .exe) — real, fixed in 4507d49. detectHostTools was the only caller of findOnPath passing a bare name; executableName is exported now and applied there, with a test that stubs process.platform to win32 and asserts a gh.exe fixture is found. Verified the test fails against the code it replaces, since ubuntu-latest alone would never catch it.

Finding 2 (toolLine hardcodes the GitHub reason) — classified low and left for the repository owner to decide, per this repo's iteration policy of not guessing on optional items. The reasoning is sound: the render path is public and takes an arbitrary HostTool[], so a caller marking git inapplicable for an unrelated reason would get a GitHub-specific sentence. It is unreachable today — detectHostTools sets applicable: false only for gh — so it is a robustness question rather than a defect, and the two candidate shapes (scope the message to gh, or carry a reason on HostTool) differ in public-API cost. Surfaced to the owner rather than picked unilaterally.

— AI Coding Agent

@github-actions github-actions Bot removed the Open OpenSpec Contains unresolved OpenSpec changes. All openspec changes must eventually reach an archive state. label Sep 23, 2026
@thecodedrift

Copy link
Copy Markdown
Member Author

Closing the loop on the low item I said was with the owner in my earlier reply.

Owner chose the reason-on-HostTool option rather than special-casing gh — reasoning being that the field should help a consuming agent reason about why a capability is unavailable, not just prevent a wrong sentence. Implemented in cfdbd82, thread resolved.

reason is optional: the type is published surface via @taskless/cli/prompts, so requiring it would break any caller already constructing a HostTool[], and an applicable tool has nothing to explain. The render prints the tool's own reason and falls back to a line naming no cause when none was given — never to the GitHub one. It also rides on info --json and the human info output, since an agent deciding whether to suggest a fix needs the cause, not just the verdict.

cli and cli-knowledge-prompts specs carry the field and three new scenarios. The change directory here is already archived, so the archived delta was updated in step with the standing specs and the two still agree.

All four checks were green before this push; will confirm again once CI settles.

— AI Coding Agent

@thecodedrift

Copy link
Copy Markdown
Member Author

@claude /review

@claude

claude Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Claude finished @thecodedrift's task in 5m 36s —— View job


Review mode: incremental — read 2 prior review thread(s) before reviewing.

Both prior threads are resolved and carry a closing reply ending in *— AI Coding Agent*, so both are treated as already addressed and are not repeated:

  • detect/host-tools.ts calling findOnPath with a bare name, breaking Windows detection — fixed in 4507d49 (now goes through the newly-exported executableName, with a win32-stubbed test).

  • toolLine hardcoding the GitHub-specific "no origin" reason for any inapplicable tool — fixed in cfdbd82 (added an optional HostTool.reason field, carried through detectHostTools, toolLine, the schema, and info output).

  • Read .prior-review.json (2 prior threads, both resolved)

  • Fetch PR diff (gh pr diff 394) and metadata

  • Read full diff plus surrounding context: detect/host-tools.ts, prompts/recipes.ts, prompts/index.ts, commands/{agent,info,onboard}.ts, schemas/info.ts, rules/platform-binary.ts, agent/onboard.md, agent/info.md, all four openspec spec deltas (standing + archived), and the four test files

  • Post findings as inline comments

Findings

1. [New] packages/cli/src/prompts/recipes.ts — prReviewSource and hostToolsStep disagree on a partial hostTools array. Posted inline at recipes.ts:415. prReviewSource's toolState(tools, "gh") treats a hostTools array that simply doesn't mention gh the same as "nothing measured" and renders the unconditioned menu bullet, while hostToolsStep only checks tools.length === 0 and takes the "Taskless already looked" branch for any non-empty array — silently omitting gh from the printed list instead of saying it wasn't measured. Both in-repo callers always supply all three tools today so this isn't reachable yet, but hostTools is published surface (@taskless/cli/prompts) and the PR's own stated goal is a reusable mechanism for future recipes with their own tool subsets — the same category of bug the reason fix in this PR just addressed for toolLine, in a different function.

Not flagged (checked, no new issue found)

  • The tools → harnesses rename: no remaining consumer in the repo reads the old key (schemas/info.ts, commands/info.ts, agent/info.md, cli.test.ts all move together; confirmed no other .tools[...] reader exists).
  • detectHostTools, toolState precedence (applicable before present), and the Windows .exe path all read correctly against the fixed code, with the win32 test exercising executableName.
  • The standing openspec/specs/cli/spec.md, cli-onboard/spec.md, and cli-agent/spec.md MODIFIED/REMOVED+ADDED blocks were diffed against the CLAUDE.md "a MODIFIED block replaces the whole requirement" warning: scenario counts only grew (73→77, 27→31, 39→41), nothing dropped; the onboard.txt→onboard.md rename correctly uses REMOVED+ADDED (title changed) rather than MODIFIED.
  • One pre-existing, out-of-scope observation: openspec/specs/cli-agent/spec.md still has several unrelated .txt references (e.g. lines 13-14, 24, 29, 35, 44, 54, 62, 209-222) describing the general recipe-file mechanism, even though every file under packages/cli/src/agent/ is now .md. This PR's docs-fix commit only corrected the onboard-specific mentions it touches; the rest predates this PR and isn't part of its diff, so not raised as a finding here.
  • commands/agent.ts's getRawRecipe/getRecipe double-lookup and commands/info.ts's double resolveRepositoryContext call (once via detectHostTools, once directly) are both minor redundant work, but the latter is explicitly justified in a code comment as "cheap enough not to be worth threading through," and neither is a correctness issue.
  • Byte-parity test coverage (onboard.test.ts) exercises all three host-tool states (present/absent/PATH-empty) across both serving paths (onboard --force vs agent onboard).

Comment thread packages/cli/src/prompts/recipes.ts
@thecodedrift

Copy link
Copy Markdown
Member Author

Re: @claude[bot] — "Claude finished @thecodedrift's task in 5m 36s …"
#394 (comment)

Finding 1 (recipes.ts — prReviewSource and hostToolsStep disagree on a partial hostTools array) was real and is fixed in 185e5ae; replied inline and resolved that thread. Reproduced it first with [{ name: "git", present: true, applicable: true }], which renders the unmeasured default bullet for gh alongside a step asserting "the list above is the answer" over a list gh is not in. The step now scopes its claim to the tools it was given and says an absent name was not looked for, which is not the same as not installed.

On the out-of-scope observation: agreed, and deliberately not touched. The other .txt references in openspec/specs/cli-agent/spec.md describe the general recipe-file mechanism and sit in requirements this change wrote no delta for, so correcting them here would be an undocumented edit to requirements outside this PR's scope. Flagged to the owner as a separate follow-up rather than folded in.

commands/agent.ts's double lookup and commands/info.ts's second resolveRepositoryContext call are both acknowledged as intentional and left as-is, per your own read of them.

— AI Coding Agent

The onboard recipe assumes `gh` in prose only, tells the agent to run a
probe the CLI can answer for free, names Linear as the expected issue
tracker, and offers PR mining in a repository that has no pull requests.

Deltas for the four capabilities that move: `cli` (the `info --json`
`tools` key becomes `harnesses`, and `tools` takes on the CLI binaries
found on PATH), `cli-onboard` (the source menu, plus the onboard.txt ->
onboard.md drift the Vale-coverage change left behind), `cli-agent` (a
third substitution flavor for whole conditional passages), and
`cli-knowledge-prompts` (the hostTools option and its full-menu default).

The embedding requirement's title named a file that has not existed for
two changes, so it is a REMOVED plus an ADDED rather than a MODIFIED: a
MODIFIED block is matched by title and a renamed one applies nothing.

Verified by archiving against a throwaway commit and diffing the
requirement and scenario lists: cli 73 -> 76 scenarios, cli-onboard
27 -> 31, cli-agent 39 -> 41, cli-knowledge-prompts 30 -> 34, with no
prior scenario lost anywhere.
`info --json`'s `tools` array has always carried agent harnesses --
Claude Code, Codex, Cursor, OpenCode -- which is the wrong noun for it
and is the name the host-tool detection in the next commit needs. The
array is unchanged in shape; only the key moves.

Every consumer in this repository moves with it: the schema, the command
(JSON and human output), the `info` recipe's example payload, and
cli.test.ts. No consumer outside this repository's control reads it from
source; the release note carries the rest.
`onboard.md` told the agent to run `command -v gh` for an answer the CLI
already has, and offered PR-comment mining in repositories with no pull
requests to mine. It now reports what was found.

- `src/detect/host-tools.ts`: presence of `gh`, `git` and `jq` via
  `findOnPath`, which walks PATH and `existsSync`es a candidate. Nothing
  is executed, nothing is hashed, no version is read. A hash tier was
  measured and dropped: GitHub publishes digests for `gh`'s release
  archives and installers rather than for the extracted binary, and a
  Homebrew `gh` 2.97.0 matched 0 of 21 official digests.
- `applicable` is a second axis, from `resolveRepositoryContext` rather
  than from a new is-this-GitHub probe that could disagree with it, and
  it outranks `present`: a repository with no GitHub origin is told
  there are nothing to mine, never told to install `gh`.
- The renderer substitutes whole passages -- bullet marker, step title
  and connective included -- the way `DETECT_EVIDENCE` already does, and
  never post-strips. With no `hostTools` supplied it renders the full
  menu, so `@taskless/cli/prompts` is unaffected.
- `agent` detects only for topics whose template contains one of these
  variables, asked of sprintf's own parse rather than of a topic list,
  so unrelated topics pay no git subprocess.
- `info --json` gains the detected tools under the freed `tools` key.
- The issue-tracker bullet stops naming Linear as the expected answer.

Topic v3 -> v4.
`findOnPath` matches a PATH entry joined with the literal string and
consults no PATHEXT, so every other caller wraps the name in
`executableName` first (platform-binary.ts:250). `detectHostTools` did
not, so on Windows `gh.exe`/`git.exe`/`jq.exe` were never found and a
user with the GitHub CLI installed was told to install it -- the one
instruction that cannot help them, arrived at from the other direction
than the `applicable` case this change already guards.

`executableName` was private; it is exported now so the pairing with
`findOnPath` lives in one place rather than being re-derived per caller.

CI runs ubuntu-latest only, so the regression is invisible without
stubbing the platform. The new test writes a `gh.exe` fixture, sets
`process.platform` to win32, and asserts it is found; it fails against
the bare `findOnPath(name)` it replaces.
This PR is the tip of its stack, so it is the last branch that can
archive before the change reaches main. The 'OpenSpec Label' job said so
in a warning annotation; landing unarchived would need a second PR to
correct, since main takes pull requests only.

Verified against the pre-archive dry run rather than trusted: the
requirement and scenario titles in all four rewritten specs are
byte-identical to what the dry run produced, and no prior scenario was
dropped anywhere.

  cli                     73 -> 76 scenarios, 20 requirements
  cli-onboard             27 -> 31 scenarios,  6 requirements
  cli-agent               39 -> 41 scenarios, 15 requirements
  cli-knowledge-prompts   30 -> 34 scenarios, 13 -> 14 requirements

The one title that disappears is the intended REMOVED plus ADDED rename
of 'Onboard recipe is embedded from help/onboard.txt', which a MODIFIED
block could not have performed: those are matched by title, so a rename
applies nothing at all.
`toolLine` printed "this repository has no GitHub origin" for any tool
with `applicable: false`. Unreachable through `detectHostTools`, which
marks only `gh` inapplicable, but this is a public render over a
caller-supplied array: a caller marking `jq` inapplicable for an
unrelated reason got a confident GitHub explanation.

`HostTool.reason` now carries it, set by the detector that knows it and
printed verbatim by the render, which never supplies one of its own.
Absent a reason the line names no cause at all, because a renderer that
fills one in is how the GitHub sentence came to be printed for tools
with nothing to do with GitHub.

OPTIONAL rather than required. The type is published surface via
`@taskless/cli/prompts`, so a required field would break every caller
already building the array, and an applicable tool has nothing to
explain — it carries no `reason` rather than an empty one a consumer
would have to test for.

The field is content, not just a guard against a wrong sentence.
`applicable: false` tells an agent to drop a capability; the reason
tells it whether any action by the user would change that, which is the
difference between staying quiet and proposing a fix that cannot work.
So it also rides on `info --json` and the human `info` output.

Standing specs updated rather than left stale: `cli` and
`cli-knowledge-prompts` gain the field and three scenarios. The change
directory on this branch is already archived, so the archived delta is
updated in step with the standing spec and the two still agree.
NOT A NORMATIVE CHANGE. The requirement 'onboard topic is registered in
the agent index' names the file the CLI embeds, and that file has been
`packages/cli/src/agent/onboard.md` since the Vale-coverage change gave
the recipes a markdown extension. The spec kept saying `onboard.txt`,
which no longer exists.

Corrected in place. This requirement is NOT one this branch's change
wrote a delta for -- that delta touches only 'Recipe substitution uses
sprintf-js named arguments' -- so there is no archived copy to keep in
step, and nothing here is paired the way `HostTool.reason` was. A later
reader should read this as a factual correction to stale prose, not as
an undocumented requirement edit: the obligation is unchanged, only the
filename it names is now the real one.

Occurrences under `openspec/changes/archive/` are deliberately left
alone. Those are the historical record of what each change said when it
landed, and rewriting them would make the archive describe a past that
did not happen. `openspec/specs/` is now free of the stale name.
`prReviewSource` asks `toolState` about `gh` by name and renders the
unmeasured default when the supplied array does not mention it.
`hostToolsStep` gated only on `tools.length === 0`, so any non-empty
array took the "Taskless already looked" branch and asserted "the list
above is the answer" over whatever it happened to contain.

On a partial array the two passages then contradicted each other: the
menu bullet reported `gh` unmeasured while the adjacent step implied the
enumeration was exhaustive, so a reader infers "not installed" from
"not listed". That is a verdict read out of silence -- the same category
as the `toolLine` defect fixed in cfdbd82, in a different function.

Unreachable through either in-repo caller, since `detectHostTools`
always returns all three tools. But `hostTools` is published surface via
`@taskless/cli/prompts` and the mechanism is meant to be reused by
recipes carrying their own subsets, so the guard belongs at the render
rather than at the one caller that happens to be exhaustive today.

The step now scopes its claim to the tools it was given and says a name
absent from the list was not looked for, which is not the same as not
installed. The full-array case still reads as a firm answer.
NOT A NORMATIVE CHANGE. Every file under `packages/cli/src/agent/` has
been `.md` since the Vale-coverage change, and the build globs
`../agent/*.md`; verified there are 22 recipe files and zero `.txt`
among them. The specs still described the mechanism in terms of `.txt`,
so the obligations are unchanged and only the filenames they name are
now the real ones.

25 occurrences across five standing specs: cli-agent (recipe lookup,
embedding, template and anonymous-variant requirements), cli (the Vite
embedding scenario), cli-knowledge-prompts (purpose, export, anonymous
variants, topic membership), cli-rules (the improve-rule recipe) and
skills (where recipes live).

NONE of them sit in a requirement this branch's change wrote a delta
for. Checked per line rather than assumed: the deltas touch 'CLI info
subcommand outputs version as JSON', 'Recipe substitution uses
sprintf-js named arguments', the three cli-onboard requirements, and an
added cli-knowledge-prompts requirement, and no corrected line falls in
any of them. So there is no archived copy to keep in step here.

DELIBERATELY LEFT: `requirements.txt` in cli-detect. That is a Python
dependency manifest, which really is a `.txt` file, and rewriting it
would introduce an error rather than remove one.

`openspec/changes/archive/` is untouched, as before: it records what
each change said when it landed.
@thecodedrift
thecodedrift force-pushed the feat/onboarding-tool-detection branch from ebf3286 to 089c7bb Compare September 23, 2026 20:46
@thecodedrift
thecodedrift merged commit e7b5775 into main Sep 23, 2026
4 checks passed
@thecodedrift
thecodedrift deleted the feat/onboarding-tool-detection branch September 23, 2026 20:49
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.

Onboarding: Make the gh CLI fully optional

1 participant