Skip to content

Latest commit

 

History

History
92 lines (80 loc) · 18.3 KB

File metadata and controls

92 lines (80 loc) · 18.3 KB

LLP 0136: Install-experience overhaul implementation plan

Type: plan Status: Implemented Related: LLP 0135, LLP 0128 Generated-by: neutral

Task breakdown for LLP 0135's six-phase hyp init wizard, the contributes.picker manifest mechanism, the ctx.commands.run seam, source-scoped export withholding, and the new @hypaware/claude-desktop plugin.

@ref LLP 0135 [implements] - refines the design's module map into ordered, independently-mergeable tasks @ref LLP 0128 [implements] - keeps the RFC's constraints (join-before-pick, additive-local-layer, attended-only) as per-task exit criteria

Framing

The design's module map has three layers with real code dependencies between them:

  1. Foundations: the contributes.picker manifest shape and the ctx.commands seam live in hypaware-plugin-kernel-types.d.ts; everything else reads or writes against them.
  2. Shared plumbing: buildPluginCatalog's pickerDescriptors, detect.js's probe dispatch, classifyClientProvenance, the ctx.commands.run runtime wiring, the descriptor-driven composePickerConfig fold, and the readRowsSince withhold resolver. Each of the three classifyClientProvenance call sites (pick-phase locking, hyp status, the export seam) and the configure phase's command dispatch depend on their respective plumbing piece landing first.
  3. The wizard itself: fork.js, join.js, pick.js, configure.js, and the index.js state machine that wires them together, plus the new @hypaware/claude-desktop plugin that supplies the first (and today only) needs_setup picker entry.

Tasks below follow that ordering: kernel-types and the catalog/detect/provenance plumbing first, then the plumbing's consumers (storage withholding, hyp status, composePickerConfig), then the wizard phase modules (which can build in parallel once their specific plumbing dependency lands), then orchestration, then the Claude Desktop plugin. The non-macOS app_bundle detection question LLP 0135#open-questions stays deferred; no task below implements it.

Every task lands with npm test green on its own. Repo style: no semicolons, no em dashes, JSDoc types (no @typedef, no inline import() types).

Completion note. T1-T10, T12, and T13 shipped with the install-experience overhaul PR (#375). T11 (orchestration) landed in a follow-up along with three items the phase modules had left open: picker manifest entries for @hypaware/openclaw and @hypaware/hermes (both use the settings_file probe - the path probe's literal is absolute-only, which a static manifest cannot express for a home-relative directory), the pick phase's managed-machine "stays on this machine" row annotation (LLP 0132 #never-silent), and the wizard's closing first-sync privacy narration. One deliberate deviation from T11's letter: runWizardFinale skips only the daemon install on the team pathway (a new skipDaemonInstall finale flag) - the restart still runs, because the wizard just wrote a new local layer the daemon must pick up.

Tasks

  • id: T1 branch: task/install-experience-overhaul/T1 deps: [] complexity: 4 -- Kernel-types and manifest schema: add PluginPickerContribution (label, summary, detect, needs_setup, configure_command) and the PickerDetectProbe union (settings_file / app_bundle / path) to hypaware-plugin-kernel-types.d.ts, change contributes.picker to an array (PluginPickerContribution[], sibling to contributes.client), add the optional attribution_column string field to the contributes.datasets[] contribution shape, and add the commands: { run(name: string, argv: string[]): Promise<number> } member to CommandRunContext (hypaware-plugin-kernel-types.d.ts:654). Types only, no runtime wiring (T4 and T5 wire the runtime). The judgment is getting the published-contract shape right on the first pass: this .d.ts is consumed by every plugin manifest and every command implementation. Update the plugin manifest JSON-schema validator (wherever contributes.client is currently validated) to accept the new array field and probe variants.
  • id: T2 branch: task/install-experience-overhaul/T2 deps: [T1] complexity: 3 -- buildPluginCatalog picker descriptors + detect.js migration: in src/core/plugin_catalog.js, read contributes.picker in the same pass that already reads contributes.client (today's lines 61-77) into a new pickerDescriptors: Map<string, PickerDescriptor> keyed by picker source id, first-manifest-wins. Replace detectClientSources's hardcoded DETECTABLE_CLIENT_SOURCES table in src/core/cli/detect.js with detectPickerSources(catalog, env), iterating catalog.pickerDescriptors with a small switch on the PickerDetectProbe variant (settings_file reuses the existing resolveClientSettingsPath parent-dir-exists check; app_bundle and path stat the literal path, path honoring the same $FOO_HOME env-override resolution hermes needs). Still best-effort: a probe failure is "not present," never thrown. Existing claude/codex detection behavior is byte-identical; prove it with the existing detect tests re-pointed at the new function.
  • id: T3 branch: task/install-experience-overhaul/T3 deps: [T2] complexity: 4 -- classifyClientProvenance(clientName, layered) in new src/core/cli/wizard/provenance.js: generalizes the central-vs-local check classifyInactiveState already does for one disabled-plugin case (src/core/cli/dispatch.js:788) into 'central' | 'local' | 'absent' for any picker source id, resolving it to its owning plugin via pickerDescriptors/clientDescriptors and checking membership in layered.centralConfig.plugins vs layered.effective.plugins. Also lands the hyp status consumer: the new "syncing: X - local-only: Y" split line in src/core/daemon/status.js, grouping picked clients by provenance so a local addition on a managed machine is never a silent state (@ref LLP 0132#never-silent). The other two call sites (pick-phase row locking, export-seam withholding) are separate tasks (T9, T5) since they land with their own consuming module. Unit-test the three-way classification directly; it is shared, correctness-critical code.
  • id: T4 branch: task/install-experience-overhaul/T4 deps: [T1] complexity: 4 -- ctx.commands.run seam: implement the CommandRunContext.commands member declared in T1 as a thin wrapper over the existing module-private runCommandByName (src/core/cli/dispatch.js:400, today used only for the config-repair redirect), populated by the dispatcher the same way ctx.skills/ctx.agents/ctx.backfills already are. Deliberately does not expose the full mutable registry to command code, just run(name, argv). init already boots with decideBootProfile(argv) === 'all-available' (dispatch.js:474), so no boot-profile change is needed for a needs_setup plugin's command to be registered by the time a caller uses this seam. No wizard code calls it yet (T10 does); test it by invoking one existing registered command through the new seam and asserting identical exit code/output to invoking it via the normal CLI path.
  • id: T5 branch: task/install-experience-overhaul/T5 deps: [T1, T2, T3] complexity: 5 -- Source-scoped export withholding: extend readRowsSince (src/core/cache/storage.js:243) with a second optional resolver, sourceWithholdResolver, threaded the same way usagePolicyResolver already is (drop-but-advance: a withheld row still moves the watermark, per the existing cwd-filter continuation semantics). Build sourceWithholdResolver once at boot from classifyClientProvenance (T3): the set of picker source ids classified 'local' on a machine with a central layer. Per-row matching uses the new attribution_column manifest field (T1); declare attribution_column: "client_name" on @hypaware/ai-gateway's ai_gateway_messages dataset contribution. A dataset with no declared attribution_column is never subject to source-scoped withholding (conservative default, matches local-only's existing design). This is the highest-risk task in the plan: it is a privacy boundary on the streaming read path, not a UI affordance. Test both the withhold-and-advance behavior and the no-attribution-column passthrough default directly against readRowsSince.
  • id: T6 branch: task/install-experience-overhaul/T6 deps: [T2] complexity: 4 -- Descriptor-driven composePickerConfig + migrate the hardcoded picker table onto manifests: replace walkthrough.js's wantsAnthropic/wantsCodex/... switch (walkthrough.js:654-729) 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 sets requires_gateway. Delete PICKER_SOURCES (walkthrough.js:308); its five entries become contributes.picker blocks (with compose) on @hypaware/claude, @hypaware/codex, @hypaware/ai-gateway (two descriptors, raw-anthropic and raw-openai, from one plugin's manifest array) and @hypaware/otel. PICKER_EXPORTS (the sink-choice half) is untouched. The exact output shape must match today's: prove it with the existing composePickerConfig tests re-pointed at the new fold, unchanged assertions.
  • id: T7 branch: task/install-experience-overhaul/T7 deps: [] complexity: 3 -- Wizard fork phase + returning-gate amendment: runWizardFork(opts) in new src/core/cli/wizard/fork.js (the top-level "Join a team" / "Local install and configuration" / quit choice), and evaluateReturningGate(opts) amending LLP 0011's gate per LLP 0129: on a managed machine, offer a scoped "adjust what this machine collects" entry (no fork, pathway preset to 'scoped') instead of dropping Reconfigure outright; on a solo machine, Reconfigure re-enters the full fork. Quit stays the default on bare enter. No picker-descriptor dependency: this phase only reads the existing hyp status summary and central-layer check. Test both gate branches (managed scoped-reconfigure, solo full-reenter) and the fork's own three choices in isolation.
  • id: T8 branch: task/install-experience-overhaul/T8 deps: [T2, T3] complexity: 4 -- Wizard join phase: runWizardJoin(opts) in new src/core/cli/wizard/join.js, a thin narration wrapper around the existing runRemoteLogin (@ref LLP 0134#login-lane, never a second enrollment mechanism). Export waitForCentralConverge as a small helper from remote_commands.js (reusing the bounded reconcile-wait runRemoteLogin already performs internally, not a second poll loop) and add classifyLoginFailure, mapping the login lane's existing no_membership / org_not_permitted / transient-network taxonomy (LLP 0058 D7) to 'failed' | 'abandoned'. On convergence, compute lockedSources as [...catalog.pickerDescriptors.keys()].filter(id => classifyClientProvenance(id, layered) === 'central') (T2's map, T3's classifier). On timeout or the no-org-config 404 steady state, narrate and return an empty lock set rather than blocking. Test the converge/timeout/failure branches and the locked-source computation against a fixture layered config.
  • id: T9 branch: task/install-experience-overhaul/T9 deps: [T2, T3, T6] complexity: 4 -- Wizard pick phase: runWizardPick(opts) in new src/core/cli/wizard/pick.js, keeping walkthrough.js's existing prompt/write/guard/overwrite-confirm shape but sourcing options from catalog.pickerDescriptors (T2) instead of PICKER_SOURCES. A row's checked state is detected.has(id) || locked.includes(id); a locked id renders disabled: true labeled · managed by your fleet (T3's classifyClientProvenance, LLP 0031 provenance vocabulary) and is filtered out of the returned sources before composition (already in the central layer; composing again would be the exact collision join-before-pick exists to avoid). Composition itself calls T6's descriptor-driven composePickerConfig. Non-interactive callers (--yes, --dry-run, presets, --from-file) invoke this phase directly, matching today's interactive = !opts.picks split (walkthrough.js:394) so every existing non-interactive picker test keeps its current shape.
  • id: T10 branch: task/install-experience-overhaul/T10 deps: [T4] complexity: 4 -- Wizard configure phase: runConfigurePhase(picked, opts) in new src/core/cli/wizard/configure.js, looping picked descriptors with needs_setup: true, narrating one at a time, running each configure_command through T4's ctx.commands.run seam, and applying the drop-on-failure rule (@ref LLP 0131#drop-on-failure: a failed or thrown configure drops that source from this run with a printed catch-up command hint, the wizard continues with the rest). Threads --print-commands through to the invoked command's own argv rather than implementing a second no-sudo escape hatch. Never runs off opts.picks (non-interactive) paths (@ref LLP 0131#attended-only). Test the success, drop-on-nonzero-exit, and drop-on-throw branches with a fake ctx.commands.run.
  • id: T11 branch: task/install-experience-overhaul/T11 deps: [T7, T8, T9, T10] complexity: 5 -- Wizard orchestration: runInitWizard(opts) in new src/core/cli/wizard/index.js, the fork -> join -> pick -> configure -> privacy -> finale state machine per the design's pseudocode, including the join-failure-returns-to-fork loop (@ref LLP 0129#failed-join-returns-to-fork), the evaluateReturningGate short-circuit for scoped re-entry, narratePrivacyIfTeamPath on the team path only (existing LLP 0100/0101 mechanism, narration-only), and runWizardFinale (existing runPickerFinale, walkthrough.js:757, plus the new joinedAlready input that skips daemon-install/attach steps hyp status already reports done from the join lane). Switches src/core/commands/init.js's runInit call site from runPickerWalkthrough(...) to runInitWizard(...). Renames the walkthrough.start/write_config/finish spans to their wizard.* equivalents and updates or removes the @ref LLP 0011#interactive-walkthrough annotation on the now-superseded runPickerWalkthrough per CLAUDE.md's ref-hygiene rule. This is the task where every prior wizard-phase task's contract gets proven together; the existing interactive walkthrough integration tests are the regression bar.
  • id: T12 branch: task/install-experience-overhaul/T12 deps: [T1] complexity: 2 -- @hypaware/claude-desktop plugin scaffold: new hypaware-core/plugins-workspace/claude-desktop/ mirroring the hermes plugin's manifest/activation shape. hypaware.plugin.json declares contributes.client (name: "claude-desktop", skill_dir/agent_dir, and deliberately no attach_probe: Desktop has no core-reversible settings file, per @ref LLP 0115#no-attach-on-join [constrained-by] and @ref LLP 0135#no-probe. An earlier revision of this task asked for one "reflecting the entrypoint: "claude-desktop-3p" attribution finding"; that was a category error, since an attach_probe is not a label but the input to probeClientAttachFromDescriptor/detachClientFromDisk. Attribution rides entrypoint per @ref LLP 0133#attribution and needs no descriptor change), contributes.picker (label: "Claude Desktop", detect: { app_bundle: "/Applications/Claude.app" }, needs_setup: true, configure_command: "claude-desktop install"), and contributes.commands (claude-desktop install, claude-desktop verify). src/index.js's activate() registers both commands (stubs are fine here; T13 fills in the implementations) and the client descriptor. No wizard-runtime dependency: this plugin is picked up generically by T2's catalog reading and T9's pick phase once both are merged, with no code change to either.
  • id: T13 branch: task/install-experience-overhaul/T13 deps: [T12] complexity: 5 -- claude-desktop install and claude-desktop verify command implementations (hypaware-core/plugins-workspace/claude-desktop/src/install.js, src/verify.js), per @ref LLP 0133#one-surface: (1) credential login chain (LLP 0117), (2) helper write (LLP 0116), (3) residue check: detect a pre-existing Claude-3p profile-directory config, back it up, clear it, unconditionally on every install (@ref LLP 0133#dialog-residue, a silent shadowed plist is a per-machine no-op at fleet scale), (4) managed-preferences plist write to /Library/Managed Preferences/com.anthropic.claudefordesktop.plist via an inline sudo prompt (@ref LLP 0133#solo-sudo), refused up front if the effective config pins an ephemeral gateway listen (127.0.0.1:0, @ref LLP 0133#consequences, LLP 0114), (5) desktop restart prompt (killall cfprefsd + relaunch hint), (6) two-tier verify: the automatic half (plist present, residue cleared) sets verify's exit code, the in-app half (send a message, confirm capture) is a printed hint only, never a blocking wizard step (@ref LLP 0131#verify-is-a-hint). Every step re-checks its own already-done state first so re-running after a bailed sudo prompt converges without re-prompting completed steps (@ref LLP 0131#idempotent-rerun) -- this idempotency is the hardest and most safety-critical part of the whole plan (real filesystem/plist/sudo side effects, a wrong residue-clear or plist write is a silent per-machine capture failure at fleet scale). --print-commands prints the privileged commands without running them.

Notes for implementers

  • T1 and T4 are the only tasks that touch the published kernel-types contract; land them first and get them right, since T2, T3, T5, T10, T12, and T13 all build on their shapes.
  • T7 (fork) is the one wizard-phase task with no picker-descriptor dependency; it can be built and merged in parallel with T2/T3/T6 if useful.
  • T5 (storage withholding) and T13 (claude-desktop install) are the two complexity-5 tasks outside the orchestration state machine itself (T11); both are privacy- or safety-critical side-effecting code, not UI affordances, and deserve the most test scrutiny per task.
  • None of these tasks implement the non-macOS app_bundle detection open question (LLP 0135#open-questions); it stays deferred until a non-Mac needs_setup client is proposed.
  • Repo prose style applies to all narration strings the wizard and the claude-desktop commands print: no em dashes, runtime strings prefer "-".