Uh oh!
There was an error while loading. Please reload this page.
Manual attach prompts to enable the client adapter (LLP 0174/0178) - #585
Conversation
Mints the executable plan for LLP 0174 (manual attach prompts to enable the client adapter). Encodes the design's settled two-change-set split as task dependencies rather than narrative order: T1/T2/T5/T7 (detection export, catalog generalization, backfill-consent export, bootstrap-floor message) have no deps and can ship alone; every phase-2 task depends on the phase-1 task it builds on, so a human can cut the branch after phase 1 lands. Also flags, as an open finding rather than silently resolving it, that CommandRunContext has no kernel handle for in-process re-activation after a config write triggered by the interactive prompt (T9).
Export dispatch.js's classifyInactiveState(layered, name) unchanged so the phase-2 attach detection work (LLP 0175 T3) can call it directly instead of re-deriving absent/disabled-local/disabled-central logic. Fix the drifted `hyp attach` usage line in clients.js's JSDoc above runAttach to match the real CLI usage string in core_commands.js (`--yes` parses nothing and was removed from attach's design per LLP 0174 #non-interactive). Add test/core/classify-inactive-state.test.js covering all three classification outcomes against hand-built layered fixtures. Task-Id: T1
Exports the existing backfill-consent prompt factory and its shared
title builder from walkthrough.js, with no behavior change, so the
interactive attach flow (T9/T10) can ask the identical backfill
question instead of re-implementing its copy.
Adds a direct-import test asserting the exported factory renders the
same prompt title/copy the init finale shows today for a sample
{ providers, retentionDays }.
Task-Id: T5Manual `hyp attach`'s endpoint-resolution ladder gives up with a message that only ever pointed at `hyp start`. That is right for an installed daemon that just isn't reachable, but useless when no daemon service is installed at all: `hyp start` runs the daemon in the foreground, it doesn't help make attach's daemon-managed path work. Check serviceDaemonStatus() and extend the give-up message to name `hyp daemon install` / `hyp daemon start` in that case, per design LLP 0174's #bootstrap-floor. Task-Id: T7
…alog buildClientDescriptorMap already built the full bundled+installed plugin catalog internally but discarded everything except clientDescriptors. Export the full buildPluginCatalog result as buildAttachPluginCatalog so the phase-2 enablement-detection and interactive-prompt work (LLP 0174 #detection) can read pickerDescriptors/pluginMetadata/knownDatasets from one catalog build instead of building it twice under two names. buildClientDescriptorMap keeps its exact signature/behavior, now a thin projection of the new function, so its existing call sites are unaffected. Task-Id: T2
New exported function co-located with composePickerConfig in
src/core/cli/walkthrough.js, resolving one PickerDescriptor's compose
contribution (requires_gateway, plugin, plugins) to the
{ requiresGateway, pluginNames, entries } shape the interactive attach
enable prompt needs, without the multi-descriptor union/upstream-merge
machinery the whole-picker fold requires. Tested against the real
bundled claude and openclaw manifests via buildPluginCatalog.
Task-Id: T6…client" `hyp attach <client>` had two failure messages that both read as "HypAware does not know this client" when the real state was "the adapter exists but is not enabled on this install": the `hypaware.ai-gateway` capability gate (no gateway-using plugin enabled at all) and the live-registry miss (some other gateway-using plugin is enabled, this one's adapter is not). Both sites now resolve three states from the static bundled+installed catalog (LLP 0174 #detection) before choosing wording: - `unknown`: nothing contributes this client, so each gate keeps exactly the text and exit code it printed before. - `not_enabled` (`absent` / `disabled-local`): the guided error naming `hyp init`, the plugin to add, the resolved config path, and `hyp daemon restart`, with `error_kind: 'adapter_not_enabled'`. - `disabled_central`: a fleet config names the plugin disabled, and LLP 0031's additive merge drops any local entry with that name, so the refusal says so rather than advising an edit that cannot work (`error_kind: 'adapter_disabled_central'`). Classification reuses T1's exported `classifyInactiveState` over `resolveLayeredConfigFromDisk`, and T2's `buildAttachPluginCatalog` for the descriptor plus the `pluginMetadata`/`knownDatasets` that layered resolution validates against. `dispatch.js` is imported dynamically: a static edge would close a cycle back through `core_commands.js` for every `hyp` invocation to serve one cold error path. Bare `hyp attach` (default `claude`) and `--json` are unchanged in shape; `--json` gains only the new `error_kind` and message. A registered client attaches exactly as before. Also: `test/core/integration.test.js`'s capability-gate test now pins the refusal's new reason (codex is catalog-known), and LLP 0175's prose placeholder `@ref LLP 0174#anchor` is respelled `#<anchor>` so the repo's own ref-hygiene gate stops reporting it as a broken annotation. Task-Id: T3
`hyp attach all` expanded only the live gateway registry, silently dropping any catalog-known client whose adapter never enabled with no explanation. Diff the live set against buildAttachPluginCatalog's clientDescriptors and print one `note:` line per catalog-known client missing from it, naming the fix (`hyp attach <name>`). Purely informational: no prompt, no error, no exit-code change, and every live-registered client still attaches exactly as before. Suppressed under --json to keep that mode's stdout to the single-line machine payload per client, matching the rest of this file's --json convention. Task-Id: T4
…g write The enable half of the LLP 0174 prompt: turn a consented "yes" into the plugin entries that activate the adapter, then bring the daemon back on the new config so the gateway registry knows the client. The write is additive rather than a compose-and-replace, because the local layer is user-owned and enabling one adapter is not a licence to rewrite the rest of it; `hyp init` stays the only flow that authors a whole config. The duplicate check runs against the effective *merged* config, so on a fleet-managed host an entry the central layer already names is never re-added locally, where LLP 0031's merge would drop it as a collision anyway. Every step reports its own outcome instead of collapsing into one boolean. A write that lands before a failed restart persists, and saying so (with the backup path the LLP 0031 guard produced) is what lets a re-run resume from the new state rather than re-ask the question. `waitForGatewayBind` joins `waitForClientAttach` in remote_commands.js with the same shape on purpose: poll a cross-process disk fact on a bounded budget, swallow a throwing probe as "not yet", and return on timeout rather than throw. Timing out is not an error here either; attach's own endpoint ladder has the better answer. Task-Id: T8
…ation runClientLifecycle's attach branch now offers to enable a not_enabled client adapter when stdin is a TTY and --json is not set, instead of failing outright: Claude/Codex get the generic enable question, OpenClaw gets the periodic-sweep disclosure variant, and a decline (or a bare Enter) exits 1 with zero side effects. The bootstrap floor (no local config file at all) skips the prompt entirely and falls through to the existing hyp init guided error, and disabled_central never reaches this branch. On accept, this calls enableClientAdapter (T8) for the guarded config write plus daemon restart/bind wait, then resolves the crux the plan left open: this same CLI invocation still has to dispatch to the newly-enabled adapter's attach(). Generalized activateSeamCommandPlugins's dependency-closure activation into an exported activatePluginDependencyClosure and exposed it through a new, narrow CommandRunContext.activatePluginClosure seam (kernel-owned, populated by the dispatcher) rather than adding a second in-process activation mechanism or re-booting/re-execing the CLI. Activation only, never a second config writer and never a network listener bind. Task-Id: T9
…porting reportEnableFailure now distinguishes a write failure (nothing else was attempted, nothing changed) from a later restart/wait failure that lands on top of a config write that already persisted: the latter names the failed step by name, states that the config change persists, names the .bak-<ts> backup path enableClientAdapter returned, and says re-running hyp attach <name> resumes from the new state instead of re-asking. No change was needed to make a re-run skip the prompt: T3's classifyInactiveState already reads the on-disk effective config, so once the write lands the client is no longer not_enabled on the next invocation and falls through to the registered-client / T7 endpoint-give-up path. Added a fixture proving that directly, plus one exercising a real restart failure (a dropped systemd/plist marker so serviceDaemonStatus reports installed, then the sandbox's absent systemctl/launchctl binary makes restartServiceDaemon throw for real) to assert the new message shape and that the enable is not retried in-invocation. Task-Id: T11
Wires T5's finale backfill consent question into T9's accept branch: after a just-enabled client's attach() succeeds in the same invocation, ask the identical "Import local <client> history now" question for its registered ctx.backfills provider (if any) and run it via runBackfillProvider on yes. Declining, or having no registered provider, leaves history unimported with no further action, matching the finale's own decline behavior exactly. OpenClaw is excluded outright: its own enable prompt already discloses the periodic sweep that imports history within ~5 minutes (LLP 0174 #openclaw), so asking again here would contradict that disclosure. The existing registered-state attach path is untouched: only a client name activated in this same invocation via T9's accept gates (tracked through activatedViaPrompt) ever reaches this step. Task-Id: T10
T10's fixture fed the backfill consent's second stdin answer on a fixed setTimeout delay after the enable answer, guessing that the async work between the two prompts (config write, plugin activation, asset materialization) would finish inside that window. When it does not - plausible on a loaded CI runner - the answer lands before the backfill consent's own readline.Interface exists, is lost to whichever interface happened to be listening (or to none), and the prompt's rl.question() then waits on a stream nothing will ever write to again: an unsettled promise that surfaces as CI's "event loop has already resolved" / cancelledByParent failure on all four tests in the file. Replace the fixed delay with an event-driven feed: the stdin fixture now writes each answer after the first only when the corresponding question has actually been printed (detected via a stdout write hook for the legacy backfill prompt's literal `[Y/n]: ` suffix), which by construction can only happen once that prompt's readline.Interface is already listening. This removes the race outright regardless of how long the intervening async work takes - verified locally by injecting an artificial 400ms delay into the test's activatePluginClosure stub and confirming all four tests still pass. Production code is unchanged; this is a test-fixture-only fix.
A separate contributor PR claims llp/0175 and llp/0176, and another branch claims 0177. The duplicate-numbers CI check compares each PR against master alone, so both pass and whichever merges second lands a duplicate number on master. Renumber this branch's plan out of the way. Mechanical only: the filename, the doc's own title line, and the five test-file comments that cite the plan alongside LLP 0174. Per CLAUDE.md this is an explicitly permitted edit on an Active doc (renumbering that does not change meaning, LLP 0156). Co-Authored-By: Claude <noreply@anthropic.com>
…ry-run Two ways T9's accept path escaped the consent promise it makes. `--dry-run` reached the prompt. Answering yes wrote the local config, restarted the daemon, and ran a real backfill import (`runBackfillProvider` is called with `dryRun: false` unconditionally), all under the one flag whose whole contract is "tell me, change nothing". `--dry-run` now refuses the question the same way `--json` and a non-TTY do, and reports the guided error instead. A plugin entry that is present but `enabled: false` also reached the prompt. `enableClientAdapter`'s write is additive by contract, so it appends nothing for a name already in the file and the disabling flag survives: the accept path wrote a byte-identical config, left a stray `.bak-<ts>` behind, printed "enabled the claude adapter (config updated)" (untrue), then fell through to the same refusal, so re-running looped forever. This is the other shape of the bootstrap floor's "never prompt when there is nothing to add to", and it is checked per requested plugin name, not just the adapter: a disabled `@hypaware/ai-gateway` starves the adapter just as effectively. Both layers count, since neither a local nor a central `enabled: false` survives an append. Neither change touches the settled T3 error copy or error kinds; both paths fall through to the refusal that was already there. Co-Authored-By: Claude <noreply@anthropic.com>
philcunliffe
commented
Aug 3, 2026
Review: findings (2 fixed, both consent/side-effect defects)Two real defects on T9's accept path, both reproduced against the live code path and both fixed. The rest of the implementation is sound: the enable write is genuinely additive and guarded, T9's mechanism choice is correct and satisfies its stated constraint, three-state detection is right at both gates, and the tests are substantive rather than string-theatre. Findings1. blocker (fixed) -
Fixed in 2. major (fixed) - the accept path was an unbreakable loop for a present-but-
The config was rewritten byte-identically, a stray Fixed in Residual, left unfixed (needs a design call): a 3. minor (left unfixed) - a 4. minor (left unfixed) - behavior drift in the extracted seam. 5. minor (left unfixed) - environment-dependent fixture. Verified clean
LLP renumber (mechanical)
This resolves a collision the repo's Verification
Pushed: |
…end" Round-2 review of 4a04534. The `hasDisabledEntry` guard closed the `enabled: false` shape of "the additive write cannot deliver this enable", but not the other one it shares a symptom set with: every requested plugin already present and *not* disabled, which makes `toAppend` empty and the write a byte-identical rewrite. Reachable whenever the config already names the adapter and its gateway but the plugin still is not live in this process (an activate() that threw at boot, an unmet dependency), and on a fleet host where a local `enabled: false` entry is dropped by a central entry that enables the same name. Both land the exact defect the fix commit set out to remove: a no-op config rewrite, a stray `.bak-<ts>` file, and an untrue "enabled the claude adapter (config updated)" on the way to the same refusal. `hasDisabledEntry` becomes `enableWriteCannotDeliver` and answers both shapes, judged against the same name set `enableClientAdapter` skips on (the effective merge union the local file, so an entry the merge dropped still counts as physically present). The two guards are complementary, not nested: an adapter that is absent while its gateway is present-and-disabled has something to append yet still cannot be enabled, and a covering test pins the other direction so the floor cannot start over-refusing a legitimate append. Co-Authored-By: Claude <noreply@anthropic.com>
philcunliffe
commented
Aug 3, 2026
Review round 2: findings (1 new major, fixed)Independent re-review of Round 1 fix 1 ( |
| config | result |
|---|---|
[{ai-gateway, enabled:true},{claude, enabled:true}], plugin not live | prompt fired, config byte-identical, 1 stray backup, false "enabled the claude adapter" |
same with enabled omitted | identical |
local [{claude, enabled:false}] + central enabling both | identical (the merge drops the local entry, so enabled === false never appears in effective) |
Reachable on a real install whenever config names the adapter and its gateway but the plugin is not live in-process (an activate() that threw at boot, an unmet dependency), and on any fleet host in the third row.
Fixed in 2943865:hasDisabledEntry becomes enableWriteCannotDeliver, answering both shapes against the same name set enableClientAdapter skips on (effective merge union local file, so a merge-dropped entry still counts as physically present). The two conditions are complementary rather than nested: an adapter that is absent while its gateway is present-and-disabled has something to append yet still cannot be enabled, so both checks are needed. Two tests added, one pinning the new refusal and one pinning the opposite direction (adapter absent plus gateway present-and-enabled must still prompt and still append) so the floor cannot start over-refusing.
Renumber (b38bc1c): clean
Full diff is 6 files, 6 insertions, 6 deletions: the rename (99% similarity), the doc's own # LLP 0178: title line, and five test comments. Nothing else. grep -rn "0175" over the tree returns zero hits. llp/0174 never cites the plan by number, so the doc pair stays coherent. Collision check across every remote branch confirms 0178 is unique repo-wide, and 0175/0176/0177 (belonging to other branches) were not touched.
Conventions
No semicolons, no U+2014, no @typedef, no inline import('...') types in the delta. The one @ref added (LLP 0174#bootstrap-floor [constrained-by]) resolves to a real heading and the gloss is honest. A sweep of all 47 @refs in clients.js found every LLP 0174 anchor resolves; the four non-resolving ones are present verbatim on master and out of this delta's scope.
Left unfixed (flagging only)
The not_enabled error copy is wrong for three distinct states. Round 1 flagged the disabled-local case. Round 2 found the same copy is also wrong for a disabled dependency: stderr says "add @hypaware/claude to <path>" while the actual blocker is a fleet-disabled @hypaware/ai-gateway. In all three states the user is told to add something already present. This is settled T3 text, so changing it wants a design call (a new LLP extending 0174), not a review-time edit. Recommend a follow-up request LLP.
Gates
npm test: 3337 pass, 0 fail, 1 skipped, at2943865npm run typecheck: clean- Pushed:
2943865
philcunliffe
commented
Aug 3, 2026
Triage: shipThe review-round cap was reached with four findings unresolved. All four were classified as preferences, not true blockers: none risks wrong data, a crash, a security hole, or a perf regression. The worst of them is misleading remediation text that appears alongside a correct alternative ( Deferred to #588. The three findings that were genuine defects (the This PR is mergeable, green, and reviewed. Next tick it flips out of draft and holds for a human to merge. |
Uh oh!
There was an error while loading. Please reload this page.
Per LLP 0016 (Shipped is Active) the implementation flips the design's status so a merged change set reads as shipped. That step was missed when #585 merged (f17b091), leaving neutral observe reporting llp#174 as implementable - a false positive whose seed path would re-implement all 11 already-merged tasks against a squash-merged history that no longer names their task branches. Lifecycle move, not a content edit, so immutability holds. Fixes#589 Co-authored-by: test <test@example.com> Co-authored-by: Claude <noreply@anthropic.com>
…layer (LLP 0174) (#654) The release-battery smoke asserted that attaching with no ai-gateway capability always prints a message naming @hypaware/ai-gateway, with a client.attach span carrying error_kind=cap_missing. LLP 0174 #detection deliberately split that single failure into two states, and #585 (f17b091) implemented it: when the requested name is contributed by a bundled plugin, the capability being absent means that adapter is not enabled, not that the install lacks a gateway, so the gate now reports 'the claude adapter is not enabled on this install' with error_kind=adapter_not_enabled. Only a name no plugin contributes keeps the old cap_missing wording. The behaviour is the settled design and is covered by test/core/attach-enablement-state.test.js; the smoke's expectation was the stale half. Update the smoke to assert both halves of the split rather than just retargeting the old one: `claude` takes the adapter_not_enabled path and must name the adapter to enable, and a new dispatch of an uncontributed name proves the cap_missing wording and span are still reachable. Fixes#652 Co-authored-by: test <test@test.com>
Implements the
attach-prompts-to-enablechange set: design LLP 0174, executable plan LLP 0175.When
hyp attach <client>fails because the client's adapter plugin is not enabled, it no longer dead-ends. Interactively, the failure becomes the missing consent moment: offer to enable the adapter through the guarded config write path, restart the daemon, complete the attach, then ask the same backfill question the init finale asks. Non-interactively it fails with an error that names the remedy. Attach still never enables anything silently, and no new enable/disable verb is added.Tasks (LLP 0175)
classifyInactiveState; fix the driftedrunAttachusage JSDocbuildClientDescriptorMapintobuildAttachPluginCatalogunknown/not_enabled/disabled_central) at both attach gateshyp attach allnotes catalog-known clients whose adapter is not enableddefaultBackfillConsentPromptFactory+backfillConsentTitleresolveSingleSourceEnablement(descriptor)for the prompt's dependency listhyp daemon install/startwhen no daemon service existsenableClientAdapter- one guarded, additive, backed-up config write + restart/bind waitAll eleven task branches are verified
--no-ffmerges onintegration/attach-prompts-to-enable.Review notes
attach(). Worth reviewing that choice and its in-code rationale against design LLP 0174#prompt.prepareLocalConfigWrite(LLP 0031 backup-before-replace); the effective merged config is consulted so a central-layer entry is never duplicated locally.disabled_centralrefuses rather than prompting - a local config cannot override a fleet-managed setting.Change-Set: attach-prompts-to-enable