Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions llp/0174-attach-prompts-to-enable.design.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ instruction (LLP 0169), and refuse-on-existing still applies unchanged.
OpenClaw, `src/core/cli/wizard/pick.js` hardcodes the client list).
Related, but a separate request; this design reduces its sting by making
the follow-up `hyp attach --client openclaw` self-sufficient.
Extended-by: LLP 0177 (issue), LLP 0180 (the fix decision).

## Open questions {#open-questions}

Expand Down
92 changes: 92 additions & 0 deletions llp/0177-picker-enables-openclaw-without-attaching.issue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# LLP 0177: The init picker enables OpenClaw but never attaches it

**Type:** Issue
**Status:** Accepted
**Systems:** Onboarding, CLI, Config
**Author:** Brendan / Claude
**Date:** 2026-08-03
**Related:** LLP 0011, LLP 0135, LLP 0174, LLP 0175, LLP 0169
**Extended-by:** LLP 0180 (the fix decision)

## Summary

The walkthrough/wizard finale has a fully generic attach lane: it iterates
`clientsPicked`, resolves each client's adapter from the live gateway
registry, calls `adapter.attach()`, and asks the backfill-consent question.
OpenClaw's client registration and attach surface exist precisely so this
loop covers it like Claude and Codex (LLP 0169). But the list feeding the
loop is a hardcoded pair, in two places:

- `src/core/cli/walkthrough.js` (~477):
```js
const clientsPicked = []
if (picks.sources.includes('claude')) clientsPicked.push('claude')
if (picks.sources.includes('codex')) clientsPicked.push('codex')
```
- `src/core/cli/wizard/pick.js` (~237): the same pair against
`clientCandidates`.

Picking OpenClaw therefore enables the adapter plugin and then silently
drops it from the finale: no attach, no backfill consent, no client assets.
The install lands in "configured, not attached" limbo with a
`client_attach_missing` warning the user must notice and repair by hand.

## Why the limbo is worse for OpenClaw than it would be for the others

For Claude and Codex, enable-without-attach was never reachable from the
picker (the hardcode includes them), and enabling alone imports nothing.
OpenClaw's backfill contribution carries a `sweep` field (LLP 0170), so
mere enablement starts the 5-minute transcript sweep. The resulting half
state - sweep recording, live capture dead, status warning - is exactly the
condition observed on a real install on 2026-08-03, and it cost a full
diagnostic session to unwind (see LLP 0175's evidence trail, which begins
from this limbo).

## Root cause

Client-ness is decided by a name list copied into two call sites instead of
being read from what the picked entries contribute. The authoritative fact
already exists per adapter: the manifest's `contributes.client.name`
(OpenClaw's declares `openclaw`), and the finale already builds a
client-descriptor map for its skills/agents leg, so the source of truth is
present in the flow that mishandles it.

## Fix direction

1. Preferred: derive `clientsPicked` from the picked entries' client
contributions (manifest `contributes.client`, or the existing
walkthrough client-descriptor map), removing the name list from both
sites. A future client adapter then joins the finale by declaring
itself, with no CLI edits - the same living-list posture LLP 0161/0162
take elsewhere.
2. Minimal fallback: add `openclaw` to both hardcodes. Rejected as the
endpoint (it is the third copy of the list and this issue is the list
biting), but acceptable as a hotfix if the derivation is delayed.
3. Verification: wizard/walkthrough unit tests asserting a picked
openclaw reaches the finale attach loop and the backfill-consent list,
plus the `walkthrough_picker_to_first_query` smoke staying green.

## Relationship to LLP 0174

LLP 0174 (accepted) closes the MANUAL path into the same limbo: a
`hyp attach` against a not-enabled adapter stops dead-ending and prompts to
enable. This issue closes the PICKER path: an enabled-by-pick adapter must
not skip attach. Together they remove every route into
enabled-but-not-attached that the user did not explicitly choose.
`hyp status`'s `client_attach_missing` warning remains the backstop for
states created deliberately (or by older installs), not the primary UX.

## Non-goals

- The capture-layer defects the limbo exposed (misattribution, settlement,
Responses decoding) are LLP 0175 / LLP 0176, fixed separately.
- Attach verification semantics ("attached" meaning settings written, not
traffic observed) stay with LLP 0175/0176's open questions.

## References

- LLP 0011 (init finale), LLP 0135 (wizard orchestration): the flow that
owns the hardcode.
- LLP 0169: the OpenClaw attach surface the finale should be calling.
- LLP 0174: the sibling consent design for the manual path.
- LLP 0175: the live investigation that started from this limbo.
132 changes: 132 additions & 0 deletions llp/0180-finale-attaches-openclaw.decision.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# LLP 0180: The wizard finale attaches OpenClaw

**Type:** Decision
**Status:** Accepted
**Systems:** Onboarding, CLI, Config
**Author:** Brendan / Claude
**Date:** 2026-08-03
**Related:** LLP 0177 (the issue this resolves), LLP 0169 (OpenClaw attach surface), LLP 0174 (manual-path sibling; its non-goal), LLP 0170 (sweep consent stance), LLP 0130 (manifest-sourced picker), LLP 0011 (init finale), LLP 0135 (wizard orchestration), LLP 0115 (Claude Desktop is not an attach-on-join client)

> Picking OpenClaw in `hyp init` enables the adapter and then drops it
> from the finale: no attach, no import, a `client_attach_missing` limbo
> (LLP 0177). The fix derives the finale's client list from what the
> picked rows' plugins contribute instead of a hardcoded pair, and keeps
> the backfill-consent question honest for a sweep-backed provider by
> replacing its question with a disclosure plus an immediate first import.

## Context {#context}

The finale's attach lane is already generic: it iterates `clientsPicked`,
resolves each client's adapter from the live gateway registry, and calls
`adapter.attach()`. OpenClaw's adapter registers into exactly that
registry (LLP 0169). The defect is upstream: `clientsPicked` is built by
checking for the literal names `claude` and `codex`, in two places
(`src/core/cli/walkthrough.js` and `src/core/cli/wizard/pick.js`), so a
picked OpenClaw never reaches the lane. LLP 0177 records the resulting
limbo and its cost; LLP 0174 closed the manual-attach route into the same
state and deferred this picker route as a non-goal.

One consent wrinkle is specific to OpenClaw. The finale's backfill prompt
offers "No - skip for now. You can import later with hyp backfill." For
Claude and Codex that is true: their contributions carry no `sweep`
field, so declining really does leave history unimported. OpenClaw's
contribution declares a sweep schedule (LLP 0170), so once the pick has
enabled the plugin, the daemon imports the existing `~/.openclaw` session
history within the sweep interval regardless of any answer. Asking the
question as-is would promise a control the wizard does not have.

## Options considered {#options}

1. **Derive `clientsPicked` from manifest client contributions.** A
picked row is a client pick iff its owning plugin contributes a
client (`contributes.client`); the finale list is those clients'
names. LLP 0177's preferred direction: the living-list posture the
picker itself already took in LLP 0130.
2. **Add `openclaw` to both hardcodes.** Rejected by LLP 0177 as the
endpoint: it mints a third copy of the list whose staleness is the
defect being fixed.
3. **For the backfill question**: (a) include OpenClaw in the consent
prompt as written; (b) exclude it from the prompt, disclose the
sweep, and run the first import in the finale; (c) exclude it and
leave the first import to the sweep. (a) is dishonest per
{#context}. (c) is honest but makes the walkthrough's first query
race a timer for no benefit.

## Decision {#decision}

- **Derivation, not enumeration (option 1).** Both sites build
`clientsPicked` through one shared helper: picked source ids resolve
through the picker descriptors to their owning plugins, and every
client descriptor whose plugin is in that set contributes its client
name. The `('claude'|'codex')` unions widen to `string[]`; client
names are data from manifests, not a closed type. A future adapter
joins the finale by declaring `contributes.client`, with no CLI edit.
- **Not every client contribution is attachable.** `contributes.client`
also covers a plugin that owns skill/agent dirs but deliberately
registers no runtime adapter (Claude Desktop, whose managed plist is
placed only by its attended `configure_command`, never attach-on-join,
LLP 0115). Such a client still belongs in `clientsPicked` (its skills
and agents install through the asset lane), but the attach lane treats
a client with no registered adapter as not applicable, never as
failed: the summary records it as `noAdapter` and the run summary
prints nothing for it. Its setup path is its picker row's
`configure_command`, which the wizard's configure phase already runs.
- **The derivation fans out row to plugin to all of that plugin's
clients.** The invariant this rests on: every client-contributing
plugin owns exactly one picker row, so a pick maps only to its own
clients. A plugin that adds a second picker row beside
`contributes.client` would make either row a pick of all its clients;
a plugin in that position needs a finer mapping here first.
- **Sweep-backed providers get a disclosure, not a question (3b).** The
finale partitions picked backfill providers by whether their
contribution declares `sweep` (exposed on the runner as `sweeping`).
Only non-sweep providers appear in the consent prompt; a sweep-backed
provider prints a one-line disclosure that the enabled sweep imports
its history on schedule, and runs the first import immediately
(dry-run aware). This is the same import the sweep would perform
within the interval, brought forward so first queries see data;
consent for it was the pick itself, whose row summary discloses the
sweep (mirroring LLP 0174#openclaw's disclosure posture for the
manual path). A cancelled consent prompt still cancels the whole
backfill step, sweep-backed providers included: cancel means "stop
the wizard", not "skip the question".
- **No wizard copy of the restart instruction.** The finale dispatches
to the adapter's `attach()`, and LLP 0169 already requires both
attach surfaces to print the `openclaw gateway restart` instruction;
the wizard inherits it by calling the same code.
- **Failure handling is already correct.** The attach lane's
warn-and-continue catch covers OpenClaw's refuse-on-existing and
not-installed hard failures; a refusal warns and the wizard proceeds,
as a join-time refusal does (LLP 0169).

## Consequences {#consequences}

- LLP 0177 is resolved; together with LLP 0174 every route into
enabled-but-not-attached that the user did not explicitly choose is
closed. `hyp status`'s `client_attach_missing` warning remains the
backstop for deliberate states and older installs.
- LLP 0174's non-goal bullet gains a forward-ref to this decision
(mechanical edit).
- `PickerBackfillRunner` gains `sweeping: string[]`. Existing fake
runners in tests that omit it behave as before (nothing sweep-backed,
every provider asked).
- The consent prompt's provider list can now be empty while backfill
still runs (an OpenClaw-only pick): no question is asked, the
disclosure and import still happen.
- The derivation widens `clientsPicked` by two clients against the
bundled catalog: `openclaw` (the fix) and `claude-desktop` (whose
skills and agents now install when its row is picked, and whose
adapterless attach is recorded as not applicable per {#decision}).
- Verification per LLP 0177: unit tests assert a picked OpenClaw
reaches the attach lane and the sweep-disclosure path, and the
`walkthrough_picker_to_first_query` smoke stays green. The full
derived set is pinned against the bundled catalog, and the backfill
runner's `sweeping` wiring is asserted against the real provider
contributions, so widening either is a deliberate test edit.

## References

- LLP 0177 (issue), LLP 0169, LLP 0174, LLP 0170, LLP 0130, LLP 0011,
LLP 0135, LLP 0115
- `src/core/cli/walkthrough.js`, `src/core/cli/wizard/pick.js` (the two
derivation sites), `src/core/commands/init.js` (the backfill runner)
23 changes: 18 additions & 5 deletions src/core/cli/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,13 @@ export interface BackfillFinaleResult {
*/
export interface PickerBackfillRunner {
available: string[]
/**
* Provider names whose contribution declares a daemon sweep schedule
* (LLP 0170). The finale never asks backfill consent for these: the
* sweep imports their history regardless of any answer, so they get a
* disclosure and an immediate first import instead (LLP 0180).
*/
sweeping?: string[]
run(args: {
provider: string
dryRun: boolean
Expand Down Expand Up @@ -223,10 +230,16 @@ export interface FinaleSummary {
binPath?: string
packageSpec?: string
}
/** `skipped` marks a client the wizard left alone because `hyp status` already reported it attached (team pathway). */
attach: { client: 'claude' | 'codex'; dryRun: boolean; ok: boolean; skipped?: boolean }[]
skillsInstalled: { name: string; client: 'claude' | 'codex'; dest: string; dryRun: boolean }[]
agentsInstalled: { name: string; client: 'claude' | 'codex'; dest: string; dryRun: boolean }[]
/**
* `skipped` marks a client the wizard left alone because `hyp status`
* already reported it attached (team pathway). `noAdapter` marks a client
* contribution with no registered runtime adapter (Claude Desktop's
* deliberate no-attach-on-join posture): not applicable to the attach
* lane, so `ok: true` and the run summary prints nothing for it.
*/
attach: { client: string; dryRun: boolean; ok: boolean; skipped?: boolean; noAdapter?: boolean }[]
skillsInstalled: { name: string; client: string; dest: string; dryRun: boolean }[]
agentsInstalled: { name: string; client: string; dest: string; dryRun: boolean }[]
daemonRestart: { skipped: boolean; dryRun: boolean; ok: boolean }
/** Per-provider onboarding backfill outcomes (empty when none ran). */
backfill: BackfillFinaleResult[]
Expand All @@ -238,7 +251,7 @@ export interface PickerWalkthroughResult {
config: HypAwareV2Config
sourcesPicked: PickerSource[]
exportPicked: PickerExport
clientsPicked: ('claude' | 'codex')[]
clientsPicked: string[]
retentionDays: number
finale?: FinaleSummary
}
Expand Down
Loading
Loading