Skip to content

Manual attach prompts to enable the client adapter (LLP 0174/0178) - #585

Merged
platypii merged 27 commits into
masterfrom
integration/attach-prompts-to-enable
Aug 4, 2026
Merged

Manual attach prompts to enable the client adapter (LLP 0174/0178)#585
platypii merged 27 commits into
masterfrom
integration/attach-prompts-to-enable

Conversation

@philcunliffe

@philcunliffephilcunliffe commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Implements the attach-prompts-to-enable change 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)

taskwhat
T1Export classifyInactiveState; fix the drifted runAttach usage JSDoc
T2Generalize buildClientDescriptorMap into buildAttachPluginCatalog
T3Three-state detection (unknown / not_enabled / disabled_central) at both attach gates
T4hyp attach all notes catalog-known clients whose adapter is not enabled
T5Export defaultBackfillConsentPromptFactory + backfillConsentTitle
T6resolveSingleSourceEnablement(descriptor) for the prompt's dependency list
T7Name hyp daemon install / start when no daemon service exists
T8enableClientAdapter - one guarded, additive, backed-up config write + restart/bind wait
T9The enable prompt, the bootstrap floor, and in-process dispatch to the newly-enabled adapter
T10Backfill consent after the accept-and-enable path
T11Per-step failure reporting and resumability on the accept path

All eleven task branches are verified --no-ff merges on integration/attach-prompts-to-enable.

Review notes

  • T9 carried the one decision the plan deliberately left open - how this same CLI invocation reaches the just-enabled adapter's attach(). Worth reviewing that choice and its in-code rationale against design LLP 0174 #prompt.
  • The config write path is additive and guarded by prepareLocalConfigWrite (LLP 0031 backup-before-replace); the effective merged config is consulted so a central-layer entry is never duplicated locally.
  • disabled_central refuses rather than prompting - a local config cannot override a fleet-managed setting.

Change-Set: attach-prompts-to-enable

testand others added 24 commits August 3, 2026 19:34
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: T5
Manual `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.
neutraland others added 2 commits August 3, 2026 23:39
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

Copy link
Copy Markdown
ContributorAuthor

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.

Findings

1. blocker (fixed) - hyp attach <client> --dry-run performed the full enable, restart, and a real backfill import.

maybeInteractiveEnableAttach gated only on parsed.json || !isTty(ctx.stdin), never on parsed.dryRun. Reproduced: with a TTY and a not_enabled adapter, --dry-run plus y wrote the local config, created a .bak-<ts>, restarted the daemon, then ran runBackfillProvider({ dryRun: false }), a real history import, under the one flag whose entire contract is "change nothing". Exit code 0.

Fixed in 4a04534: --dry-run now refuses the question exactly as --json and non-TTY do, falling through to the settled T3 guided error. Regression test asserts exit 1, no prompt copy, byte-identical config, zero backups.

2. major (fixed) - the accept path was an unbreakable loop for a present-but-enabled: false entry.

classifyInactiveState returns disabled-local and T3 maps it to not_enabled (as the plan settled), so it reached the prompt. But enableClientAdapter's write is additive by contract: client_enable.js:138 filters against a present set built from the effective and local plugin names, so it appended nothing for a name already in the file, and the enabled: false flag survived untouched. Reproduced:

Enable @hypaware/claude (and @hypaware/ai-gateway) now? [y/N]
error: enabled the claude adapter (config updated), but could not activate it in this process; re-run 'hyp attach claude' to finish
error: the claude adapter is not enabled on this install; ... or add @hypaware/claude to <path> ...

The config was rewritten byte-identically, a stray .bak- was left behind, "enabled the claude adapter" was untrue, and the advice to "add" an entry that already exists loops forever.

Fixed in 4a04534: a hasDisabledEntry guard applies the same floor the missing-config case already establishes, never prompt when there is nothing to add to. Checked per requested plugin name rather than only the adapter (a disabled @hypaware/ai-gateway starves the adapter just as effectively), and read from the effective merge so both local and central enabled: false count. Two regression tests cover the adapter-disabled and gateway-disabled shapes.

Residual, left unfixed (needs a design call): a disabled-local user now gets T3's settled message telling them to "add @hypaware/claude to <config>" when the entry already exists, disabled. hyp init (also named) does repair it, and hyp <cmd> dispatch already has correct copy for this state (dispatch.js:270, "set \"enabled\": true on the ... entry"). Whether attach should reuse that wording, or offer to flip the flag under the same consent prompt, changes text T3 settled. Worth a follow-up request.

3. minor (left unfixed) - a @ref anchor that does not resolve, propagated. The branch adds two @ref LLP 0139#seam-fresh-activation annotations. llp/0139-desktop-picker-consent.decision.md has only ## Context / ## Decision / ## Consequences; nothing slugs to seam-fresh-activation. Pre-existing (master already carries it at dispatch.js:778), so the branch is at least consistent. Repairing it should be one sweep over all four sites, not a half-change here. Every other anchor the branch cites resolves.

4. minor (left unfixed) - behavior drift in the extracted seam.activateSeamCommandPlugins previously bailed before calling activatePlugins when the owner was eliminated by dependency resolution; activatePluginDependencyClosure now activates surviving closure members anyway, with the caller's equivalent guard running after. Only reachable for a config-selected plugin whose resolution eliminates the owner but not its closure, and the activated plugins are ones the config names either way. No correctness impact I can construct; touching it is churn.

5. minor (left unfixed) - environment-dependent fixture.test/core/attach-enable-resume.test.js:206 relies on restartServiceDaemon genuinely throwing because no systemctl/launchctl is reachable. The comment acknowledges this and argues it makes the failure real rather than simulated, which is defensible, but the test's meaning changes on a host that has a service manager.

Verified clean

  • Additive write. Unrelated plugins[], sinks, query survive byte-identically; backup content equals the pre-write file; entry order is append-only. Effective-merge dedupe proven against a real central seed and a local duplicate. prepareLocalConfigWrite is called before the replace and its backupPath threaded into the result. A restart throw leaves steps: {write:'ok', restart:'failed', wait:'n/a'} with the write persisted, and reportEnableFailure names the step, the backup path, and the resume instruction.
  • T9's mechanism. Option (a): activateSeamCommandPlugins's closure activation generalized into exported activatePluginDependencyClosure, surfaced as CommandRunContext.activatePluginClosure, documented in-code at all three sites plus the kernel-types member. The no-second-listener constraint holds: ai-gateway/src/index.js:32 states the source listener is not bound at activation, and activatePlugins starts no sources. ctx.plugins is the same activePlugins array the closure pushes into, so the allLive check is real and not vacuously false.
  • Consent integrity. Non-TTY and --json return before any write, so decline and every refusal are zero-side-effect by construction rather than by cleanup. Decline exits 1 with config unchanged and no backup. Bootstrap floor skips the prompt and shows the hyp init error. disabled_central never reaches the prompt and refuses with the fleet message at both gates.
  • Three-state detection at both gates, including that an unrecognized name still gets the plain unknown client text at both sites. hyp detach returns before the gate, so none of this leaks into detach.
  • The 63db2ab race repair is sound.feedNext() is driven by the second question's own [Y/n]: reaching stdout, not a timer, so it cannot race the intervening async work; the first answer is pre-buffered into a paused PassThrough. The accept-path tests do not trivially pass: attach-enable-prompt.test.js:169 asserts the on-disk config gained both plugins, that ctx.plugins carries the adapter, and that client.attach() wrote its file with the real endpoint. Backfill suite run 5x consecutively, green each time.
  • Conventions. No em dashes, no code semicolons, no @typedef, no inline import('...') types, all @import specifiers repo-root-anchored .js paths.

LLP renumber (mechanical)

b38bc1c: llp/0175-attach-prompts-to-enable.plan.md moved to llp/0178-..., title line updated, and five test-file comments citing the plan retargeted. llp/0174-...design.md needed no change (no forward-ref to the plan); no @ref LLP 0175 annotations existed in source. git grep 0175 on the pushed tree returns nothing.

This resolves a collision the repo's duplicate-numbers check cannot catch: PR #586 also adds an llp/0175-*, and that check validates each PR against master in isolation, so it passes on both and the duplicate would only appear on master after the second merge. Neutral yielded the number rather than pushing churn onto the contributor's branch; #586 keeps 0175 and 0176.

Verification

  • npm test: 3336 tests, 3335 pass, 0 fail, 1 skipped (+3 new tests from the two fixes)
  • npm run typecheck: clean
  • Both fixes positively verified in the committed tree (parsed.dryRun gate at clients.js:624, hasDisabledEntry at :649 and :840), not merely by a green suite.

Pushed: b38bc1c (renumber), 4a04534 (fixes, head).

…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

Copy link
Copy Markdown
ContributorAuthor

Review round 2: findings (1 new major, fixed)

Independent re-review of 63db2ab..4a04534, deliberately run by a different agent than the one that wrote the round 1 fixes. Both round 1 fixes are real and correctly implemented, and both regression tests genuinely fail when reverted. One new finding: fix 2's guard was too narrow and left the identical defect reachable through a second trigger. Fixed and pushed as 2943865.

Round 1 fix 1 (--dry-run): confirmed sound

clients.js:624 refuses the prompt on parsed.dryRun alongside --json and non-TTY. Verified by direct execution rather than by the suite: hyp attach claude --dry-run on a not_enabled state exits 1, writes no config, leaves no .bak-, performs no restart, never reaches runBackfillProvider, and prints the unchanged T3 guided error. The guard sits after the state/all/json/TTY checks and before any disk read, so no other path changed.

Revert test: removing line 624 makes the --dry-run never prompts test fail (0 !== 1, the prompt fired and attach completed with exit 0). The test proves the fix.

Round 1 fix 2 (hasDisabledEntry): sound, but incomplete

Confirmed real and correctly targeted. The defect reproduces with the guard removed: byte-identical config, one stray .bak-, a false "enabled the claude adapter" message, and advice to add an entry already in the file. The guard is genuinely per-plugin-name (a disabled @hypaware/ai-gateway with the adapter absent is caught) and genuinely reads the effective merge (a central enabled: false counts). No collateral damage: the accept path, decline, bootstrap floor, disabled_central, --json and non-TTY all behave as before.

Revert test: removing the guard call fails both new tests. They prove the fix.

New finding (major, fixed): the guard missed the other half of its own invariant

clients.js:840-855 at 4a04534 tested only entry.enabled === false. But enableClientAdapter skips any name already in effective union local fileregardless of the flag, so toAppend is also empty when every requested name is present and not disabled. That produces the identical symptom set the commit set out to remove.

Reproduced at 4a04534 with the round 1 fixes in place:

configresult
[{ai-gateway, enabled:true},{claude, enabled:true}], plugin not liveprompt fired, config byte-identical, 1 stray backup, false "enabled the claude adapter"
same with enabled omittedidentical
local [{claude, enabled:false}] + central enabling bothidentical (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, at 2943865
  • npm run typecheck: clean
  • Pushed: 2943865

@philcunliffephilcunliffe changed the title Manual attach prompts to enable the client adapter (LLP 0174/0175)Manual attach prompts to enable the client adapter (LLP 0174/0178)Aug 3, 2026
@philcunliffe

Copy link
Copy Markdown
ContributorAuthor

Triage: ship

The 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 (hyp init) in the same message, so a user is under-served rather than dead-ended, which preserves this change set's whole purpose.

Deferred to #588. The three findings that were genuine defects (the --dry-run blocker and two shapes of the undeliverable enable prompt) were fixed in this PR, not deferred.

This PR is mergeable, green, and reviewed. Next tick it flips out of draft and holds for a human to merge.

@philcunliffe
philcunliffe marked this pull request as ready for review August 3, 2026 23:59
@philcunliffephilcunliffe added the neutral:approved neutral reviewed this and holds it for a maintainer merge (own or adopted PR; LLP 0025/0030) label Aug 3, 2026
@platypii
platypii merged commit f17b091 into masterAug 4, 2026
9 checks passed
@platypii
platypii deleted the integration/attach-prompts-to-enable branch August 4, 2026 00:35
philcunliffe added a commit that referenced this pull request Aug 4, 2026
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>
philcunliffe added a commit that referenced this pull request Aug 7, 2026
…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>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

neutral:approvedneutral reviewed this and holds it for a maintainer merge (own or adopted PR; LLP 0025/0030)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@philcunliffe@platypii