Install-experience overhaul: guided wizard with join-team and local pathways (LLP 0128)#375
Conversation
…er validation
Add the published-contract shapes the install-experience overhaul builds on,
types only (T4/T5 wire the runtime):
- `PluginPickerContribution` (label, summary?, detect?, needs_setup?,
configure_command?) and the `PickerDetectProbe` union (settings_file /
app_bundle / path) in hypaware-plugin-kernel-types.d.ts.
- `contributes.picker: PluginPickerContribution[]`, an array sibling to
`contributes.client`, so one plugin can contribute multiple picker rows.
- Optional `attribution_column` string on `contributes.datasets[]` for
source-scoped export withholding (LLP 0132).
- Optional `commands: { run(name, argv): Promise<number> }` seam on
CommandRunContext. Optional here because the dispatcher does not populate
it yet; T4 wires the runtime and tightens it to always-present.
Teach the plugin manifest validator (validateManifest) to accept the picker
array and validate probe variants (exactly one of settings_file/app_bundle/
path, non-empty string), keeping unknown row fields (e.g. compose) opaque.
Task-Id: T1
runWizardFork(opts) in src/core/cli/wizard/fork.js implements the
init wizard's top-level pathway fork ("Join a team" / "Local install
and configuration" / quit, quit default on bare enter). evaluateReturningGate(opts)
amends LLP 0011's returning-install gate per LLP 0129: a managed
machine offers a scoped "adjust what this machine collects" entry
instead of dropping Reconfigure outright, a solo machine's Reconfigure
re-enters the full fork. The gate only reads the existing hyp status
summary and central-layer check (collectHypAwareStatus), so this phase
has no dependency on the picker-descriptor plumbing other wizard tasks
build on.
Task-Id: T7
Populate CommandRunContext.commands in the dispatcher as a thin run(name, argv) wrapper over the module-private runCommandByName, pulled off the same stdout/stderr/stdin/env/cwd/registry/kernel the dispatch already holds, the same way skills/agents/backfills are populated from the kernel. Exposes only run, never the mutable command registry. Tighten CommandRunContext.commands from optional to required now that the dispatcher always populates it (the shape T1 declared). Test invokes one registered command through the seam and asserts the exit code and stdout/stderr are byte-identical to running the same command through the normal CLI path. Task-Id: T4
…tion Declares contributes.client (name claude-desktop, skill_dir/agent_dir mirroring the embedded CLI's own .claude paths, and an attach_probe pointed at the managed-preferences plist per LLP 0133's corrected local- surface finding) and contributes.picker (label "Claude Desktop", the app_bundle detect probe, needs_setup, configure_command "claude-desktop install") on the existing claude-desktop plugin manifest, plus stub "claude-desktop install"/"claude-desktop verify" commands in activate() so the wizard's generic catalog reading and pick phase can pick this plugin up with no further code change once T2/T9 land. Task-Id: T12
Reads contributes.picker in the same pass buildPluginCatalog already reads contributes.client, into a new first-manifest-wins pickerDescriptors: Map<string, PickerDescriptor> keyed by each row's name. Adds a required name: string field to PluginPickerContribution (kernel-types + manifest validator + design doc), since contributes.picker is array-shaped specifically so one plugin manifest can contribute more than one picker row (e.g. ai-gateway's raw-anthropic/raw-openai), and the plugin's own top-level manifest name can't disambiguate between them. Mirrors every other array-shaped contributes.* entry (skills, commands, agents, datasets, init_presets), which all key off a per-row name. Replaces detectClientSources's hardcoded DETECTABLE_CLIENT_SOURCES table in src/core/cli/detect.js with detectPickerSources(catalog, env), switching on the PickerDetectProbe variant (settings_file reuses resolveClientSettingsPath's parent-dir-exists check; app_bundle and path stat the literal path, path honoring the same $FOO_HOME env-override resolution). Still best-effort: a probe failure is "not present," never thrown. Existing claude/codex detection behavior is byte-identical, proven by the existing detect tests re-pointed at the new function against a fixture catalog. walkthrough.js's one call site is updated to a small defaultPickerDetect wrapper that builds a catalog and delegates to detectPickerSources; PICKER_SOURCES itself is untouched (T6's job). Task-Id: T2
Add src/core/cli/wizard/configure.js: runConfigurePhase(picked, opts) loops picked descriptors with needs_setup: true and a configure_command, narrates each one at a time, runs it in process through T4's ctx.commands.run seam, and applies the drop-on-failure rule - a non-zero exit or a throw drops that source with a printed catch-up hint and the phase continues with the rest. Threads --print-commands onto the invoked command's own argv rather than adding a second no-sudo escape hatch, and stays attended-only: a non-interactive opts.picks path runs nothing. Tests cover the success, drop-on-nonzero-exit, and drop-on-throw branches plus the --print-commands passthrough and the attended-only guard, all against a fake ctx.commands.run. Task-Id: T10
Add `classifyClientProvenance(clientName, layered, catalog)` in new `src/core/cli/wizard/provenance.js`, generalizing the central-vs-local read `classifyInactiveState` does for one disabled-plugin case into a `'central' | 'local' | 'absent'` three-way for any picker source id. It resolves the id to its owning plugin via the catalog's picker/client descriptors and checks membership in the central vs effective layers (LLP 0031). Shared, correctness-critical plumbing: the pick phase's row locking (T9), the export seam's withhold set (T5), and this task's `hyp status` consumer all read it. Land the `hyp status` consumer: the collector groups picked (configured) clients by provenance into a new `clientSync` split, and the renderer prints the "syncing: X - local-only: Y" line, so a local addition on a managed machine is never a silent state (LLP 0132 #never-silent). Null on a solo host, keeping the V1 surface unchanged. Unit-tests cover the three-way classification directly and the status split end to end. Task-Id: T3
Add the real hyp claude-desktop install and hyp claude-desktop verify command bodies (LLP 0133#one-surface), replacing the earlier stubs: - install.js runs five idempotent steps: credential login chain (LLP 0117, skipped in org_key mode, skipped if already signed in), helper write (delegates to the existing install-helper command, LLP 0116), dialog-residue check/backup/clear (Claude-3p profile directory under Application Support, unconditional on every run), managed-preferences plist write via an inline sudo prompt (LLP 0133#solo-sudo, only when the rendered plist differs from what is on disk), and a restart prompt (killall cfprefsd). Every step re-checks its own already-done state first, so a bailed sudo prompt converges on re-run without re-doing finished steps (LLP 0131#idempotent-rerun). Installation is refused up front, before any step runs, if the effective gateway listen is ephemeral (127.0.0.1:0, LLP 0114, LLP 0133#consequences). --print-commands prints the two privileged command sequences (plist write, restart) without running them. - verify.js implements the two-tier verify (LLP 0131#verify-is-a-hint): the automatic half (plist present and up to date, residue cleared) drives the exit code; the in-app half (send a message, confirm capture) is printed as a hint only and never blocks. - Extract HELPER_BASENAME/resolveHypBin/resolveHelperPath/resolveInputs out of index.js into a new inputs.js so install.js and verify.js can both depend on them without a circular import back into index.js. - Export profile.js's shellQuote so install.js can render --print-commands output with the same quoting the credential wrapper script uses. Covered by new test/plugins/claude-desktop-install.test.js and test/plugins/claude-desktop-verify.test.js (19 tests): login-chain branching by credential mode and prior sign-in state, ephemeral-listen refusal with zero side effects, residue backup/clear and its idempotent no-op re-run, plist skip-when-up-to-date vs sudo-write-when- stale, drop-on-step-failure without stranding later steps, --print-commands producing no spawn calls, and verify's automatic/ in-app split including that checkInstallState never mutates state. npm run typecheck is clean. npm test: 2550 tests, 2542 pass, 1 skip, 7 pre-existing failures in test/core/leave-command.test.js unrelated to this change (environmental: no network egress and no systemctl binary in this sandbox, confirmed present on the integration branch before this task's changes and unchanged after). Task-Id: T13
…manifests
Replace walkthrough.js's wantsAnthropic/wantsCodex switch with a fold over
each picked descriptor's own `compose` data ({ plugin, requires_gateway,
gateway_upstream }), unioning requested gateway upstreams deduped by name
and including @hypaware/ai-gateway iff any picked descriptor requires the
gateway. Delete PICKER_SOURCES; its five entries are now contributes.picker
blocks (with compose) on @hypaware/claude, @hypaware/codex, @hypaware/otel,
and @hypaware/ai-gateway (raw-anthropic + raw-openai). PICKER_EXPORTS is
untouched.
Adds PluginPickerCompose/PluginPickerGatewayUpstream to the kernel-types
contract and the `compose` field to PickerDescriptor, read in
buildPluginCatalog. A documented display-order policy keeps the picker's
row order (clients first, then raw modes, then infra) stable and
deterministic. New compose-picker-config test pins the exact config output
byte-for-byte against the real bundled manifests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Task-Id: T6
Add `src/core/cli/wizard/join.js` with `runWizardJoin(opts)`, a thin narration wrapper around the existing `runRemoteLogin` (@ref LLP 0134 #login-lane) - never a second enrollment mechanism. It runs the login lane, waits (bounded) for the daemon to converge on the org config, and computes `lockedSources` as the picker source ids the central layer owns (T2's `pickerDescriptors`, T3's `classifyClientProvenance`). On timeout or the no-org-config 404 steady state it narrates and returns an empty lock set rather than blocking (@ref LLP 0129#join-before-picker). - Export `waitForCentralConverge` from `remote_commands.js`, reusing the bounded reconcile-wait (`waitForClientAttach`) `runRemoteLogin` already performs internally instead of a second poll loop. - Add `classifyLoginFailure`, mapping the login lane's D7 taxonomy (`no_membership` / `org_not_permitted` -> definitive, vs transient) to `'failed' | 'abandoned'`; the two definitive D7 phrases are lifted to exported constants so the classifier reuses the taxonomy rather than re-encoding it. - Tests cover the converge, timeout, and login-failure branches, the locked-source computation against a fixture layered config, the D7 classification split, and `waitForCentralConverge`'s ok/timeout verdict. Task-Id: T8
Add `runWizardPick(opts)` in `src/core/cli/wizard/pick.js`, extracting the pick/compose/guard/write shape from `runPickerWalkthrough` but sourcing rows from `catalog.pickerDescriptors` (T2) instead of the retired `PICKER_SOURCES` table. A row's checked state is `detected.has(id) || locked.includes(id)`; a locked id renders `disabled: true` with the `· managed by your fleet` label (LLP 0031 provenance vocabulary) and is filtered out of the returned sources before composition, so a central-layer source is never re-composed into the local layer (LLP 0129 join-before-picker). Composition itself reuses T6's descriptor-driven `composePickerConfig`. Non-interactive callers set `opts.picks` and skip prompting/detection, matching the existing `interactive = !opts.picks` split. Support the locked rendering by threading an optional `disabled` flag through the TUI multiselect (types, reducer, render): a disabled row is context-only, not toggleable by space or select-all, and renders dimmed. Export the shared pick helpers from `walkthrough.js` for reuse rather than duplicating them. Tests: pick-phase behavior (non-interactive compose, detection pre-check, descriptor-sourced options, locked-row disable + filter, overwrite guard, cancel) plus TUI reducer/render coverage for disabled rows. Task-Id: T9
Extends readRowsSince (src/core/cache/storage.js) with a second optional resolver, sourceWithholdResolver, threaded the same way usagePolicyResolver already is: drop-but-advance, so a withheld row still moves the watermark (LLP 0070#incremental continuation semantics). createSourceWithholdResolver (src/core/cache/source-withhold.js) is a pure, catalog-agnostic factory over plain data (withheld source ids + a dataset -> attribution-column map). buildSourceWithholdResolver (src/core/runtime/source_withhold.js) does the boot-time reduction from classifyClientProvenance (T3) and the plugin catalog: the set of picker source ids classified 'local' on a machine with a central layer. bootKernel now builds the kernel runtime after the catalog and layered config are resolved (previously built at the top of the function) so it can pass this resolver through createKernelRuntime. Per-row matching uses the new attribution_column manifest field (T1). @hypaware/ai-gateway declares attribution_column: "client_name" on its ai_gateway_messages dataset contribution. A dataset with no declared attribution_column is never subject to source-scoped withholding, the conservative default matching local-only's existing design. Tests cover withhold-and-advance, the no-attribution-column passthrough default, no-resolver passthrough, projection-can't-bypass, and composition with the existing cwd-based local-only filter, all directly against readRowsSince. Task-Id: T5
…/T9' into HEAD # Conflicts: # src/core/cli/wizard/types.d.ts
… claude-desktop row Two manifest defects found in the install-experience overhaul (PR #375) that shipped "green" because tests relied on hand-written fixtures that diverged from the real bundled manifests: - claude/codex picker rows omitted the `detect` probe. The retired `DETECTABLE_CLIENT_SOURCES` table detected these via `.claude/settings.json` and `.codex/config.toml`; without the manifest probe `detectPickerSources` never pre-checks them (LLP 0136 T2/T6 required byte-identical detection). Restore `detect: { settings_file: ... }` on both rows. - The @hypaware/claude-desktop picker row omitted the required `name`. `discoverBundledPlugins` runs `validateManifest`, so the whole plugin landed in `bundled.failed` (a warning, not a boot error): every claude-desktop command (install/verify/profile/...) and the wizard's Claude Desktop picker row were silently unavailable. Add `name: "claude-desktop"` (the id the design, plan T12, and the tests already expect). Regression guards: assert the real bundled claude/codex descriptors carry their detect probes, and that no bundled manifest fails validation, so a future manifest that a fixture-based test can't catch is caught here. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Neutral review - install-experience overhaul (LLP 0128/0135/0136, T1-T13)Verdict: findings (2 actionable defects, both fixed and pushed). Reviewed the full diff Fixes pushed to Actionable findings (fixed)F1 (High) - claude-desktop plugin fails to load entirely. F2 (Medium) - claude/codex picker rows lost autodetection (T2/T6 regression). Regression guards added ( High-risk area assessments
Style complianceNo em dashes (U+2014) anywhere in the diff; no semicolons on added JS statements; TestsFull |
|
Round-2 neutral review - verdict: clean Reviewed the round-1..head delta ( F1 - claude-desktop picker row dropped by manifest validation: FIXED (correct + complete).
F2 - claude/codex detection parity: FIXED (correct + complete).
Regression guards (
No new actionable findings. Full file Did not ready/merge/label. |
Implements the install-experience overhaul from RFC LLP 0128 (design LLP 0135, plan LLP 0136), reworking
hyp init's first-run walkthrough into a guided wizard with a top-level fork (Join a team / Local install) and a distinct configure phase for sources needing interactive setup.What landed (13 tasks, all verified-merged --no-ff)
PluginPickerContribution[],PickerDetectProbeunion, datasetattribution_column,CommandRunContext.commands.run.buildPluginCatalogpicker descriptors +detect.jsmigration off the hardcodedDETECTABLE_CLIENT_SOURCES.classifyClientProvenance(central/local/absent) +hyp statussyncing/local-only split line (LLP 0132).ctx.commands.run(name, argv)in-process command-dispatch seam.readRowsSinceviasourceWithholdResolver+attribution_column(privacy boundary, LLP 0132).composePickerConfig;PICKER_SOURCESmigrated onto plugin manifests.runInitWizard),init.jscall-site switch, telemetry rename.@hypaware/claude-desktopplugin scaffold.claude-desktop install/verifyimplementations (LLP 0133): login, helper write, residue clear, managed-preferences plist via sudo, restart, two-tier verify.Notes
app_bundledetection probe (Claude Desktop is Mac-only today) - recorded as an open question in LLP 0135.Change-Set: install-experience-overhaul