Type: plan Status: Active Systems: Config, Daemon, Onboarding, Sources, Gateway Author: neutral Date: 2026-06-26 Related: 0044, 0045 Generated-by: neutral
LLP 0045 is the implementation design for client attach on join, the reversible instance of the LLP 0036 action seam (decision: LLP 0044). It already names the files and functions to add or change and groups them into nine independently-mergeable seams. This plan turns those seams into a task graph with real code dependency edges, so the first wave can parallelize and each task merges on its own without leaving the tree broken.
The design's "Module / seam breakdown" lists nine modules. This plan keeps that exact decomposition, it is already minimal and each seam is independently testable, maps each task 1:1 onto the design's seam numbering for traceability, and only makes the dependency edges explicit so neutral can schedule them. A tenth task (T10) is the lifecycle move that marks 0045 shipped once the substance has merged.
The shape of the graph:
-
Foundational / independent (deps
[]). Four tasks have no in-repo dependency on each other and form the first wave:- T1:
attach()writes a self-describing undo record. Adapter-local edits to the claude and codexattach()paths so each marker carries everything the single core undo (T4) needs to reverse without the plugin loaded, claude's_hypawaremarker (hypaware-core/plugins-workspace/claude/src/settings.js,MARKER_KEY; surfaced viawriteAttachOutput/anthropic.js) recordsprev_base_urlplus the managedANTHROPIC_BASE_URL/SessionStarthook entries; codex's# BEGIN/END hypawaremarked block (hypaware-core/plugins-workspace/codex/src/settings.js) records the priormodel_provider. Unit-tested via the marker contents. Independent, it touches only adapter files and changes no core contract (thejson: trueone-line output shape is unchanged). - T2: attach policy reader. New
src/core/config/attach_policy.js(readAttachPolicy, tri-state overconfig.attach.on_join), thebackfill_policy.jstwin, the single source of truth shared by the reconciler handler (T6) and the status surface (T9) so the two can never disagree on what a block means. Pure; unit-tested. - T3: reconcile-context seam. Extend
ActionContextandReconcileInputinsrc/core/config/types.d.tswith the three optional fields the attach handler reads,clientDescriptors?: Map<string, ClientDescriptor>(enumerate / client→plugin map),clients?: AiGatewayCapability(invoke the effect), andendpoint?: string(the local gateway base URL). Type-only; no behaviour change until a handler (T6) or the daemon (T7) reads them. - T8: per-plugin
attachconfig validation. AvalidateAttachSectionbesidevalidateBackfillSectionin the claude/codexconfig.js, wired into each plugin's config-section validator (validateClaudeConfig/ the codex equivalent). Validatesconfig.attach.on_join; plugin-local, no top-level/core schema change. It only needs to land before the end-to-end smoke proves an opt-out, not before any core code compiles.
- T1:
-
The single core undo (deps
[T1]). T4: the one detach implementation. Newsrc/core/config/client_detach_disk.js: reverse a client's attach from the descriptor'sattachProbe+ the marker undo record, format-aware (jsonmarker-key /tomlmanaged-block) but plugin-agnostic, reusingresolveClientSettingsPath(src/core/daemon/client_settings_path.js) and theprobeClientAttachedformat logic (src/core/daemon/status.js). It subsumes the adapters' olddetach(), including the Codex marked-block strip and priormodel_providerrestore, so it replays exactly the undo record T1 writes (hence the edge on T1). Unit-tested on fixture settings files with no plugin loaded, proving reverse never depends onctx.clients. -
Retire the old detach (deps
[T4]). T5: drop adapterdetach()+ reroute manualhyp detach. RemovedetachfromAiGatewayClientRegistration(hypaware-plugin-kernel-types.d.ts) and from the claude/codexregisterClient(...)calls, and pointrunClientLifecycle's detach branch (src/core/cli/core_commands.js) at the T4 core undo, resolved via theclientDescriptor. There is then exactly one undo and it cannot drift from itself. The existingclaude_attach_detach/client_attach_idempotentsmokes now exercise the core undo and must stay green: they are the cross-format regression for the single undo. -
The attach handler (deps
[T2, T3, T4]). T6:action_attach.js. Newsrc/core/config/action_attach.js(createAttachHandler+attachHandler), mirroringaction_backfill.js:desired()iteratesctx.clientDescriptors∩ enabled plugins ∩readAttachPolicy(T2), guarded onctx.clients.getClient(name)being present (T3);perform()callsattach({ endpoint: ctx.endpoint, json: true }), parses the one-line JSON, and records the marker detail (settings_path,prev_value);reverse()invokes the T4 disk-driven undo (it does not callctx.clients, which lacks the dropped client). Unit-tested with injected fakeclientDescriptors+clients+ filesystem. -
Daemon wiring (deps
[T3, T6]). T7:runtime.js. Insrc/core/daemon/runtime.js: resolveclientDescriptors(from the plugin catalog),clients/endpoint(fromboot.runtime.capabilitieswhen the gateway capability is present, viagateway.localEndpoint()with theconfiguredGatewayEndpointfallback the CLI already uses), thread all three onto the reconcile context (T3), and register the handlers[attachHandler, backfillHandler]: attach first so in-process live-capture wiring starts ahead of the (possibly multi-minute) backfill subprocess. Needs both the context fields (T3) and the handler it registers (T6). -
Status surface (deps
[T2]). T9: declared-attach targets.buildClientActionsReport(src/core/daemon/status.js) gains a declared-attach-targets derivation symmetric to backfill's, reusing theclientDescriptors-derived plugin set status already builds (backfillPlugins) andreadAttachPolicy(T2) for the interpretation: an enabled client plugin on a joined host (hasCentral) with no marker renderspending, withattach.on_join: falseor on a non-joined host rendersn/a, and adonemarker renders attached. A failed/pending attach must not flipoveralltodegraded. It renders the genericdone/failedattachmarkers (the handler writes them) unchanged; it never runs a pass, so it depends only on the shared policy reader. -
Lifecycle flip (deps on all substance). T10 flips
llp/0045-client-attach.design.mdStatusAccepted → Active: the shipped marker, landed only once the whole change set has merged.
This yields a 4-wide first wave (T1, T2, T3, T8), then T4 (behind T1), then a
fan-out of T5 (behind T4), T6 (behind T2/T3/T4) and T9 (behind T2), the single
integration task T7 (behind T3/T6), and finally the lifecycle flip T10. Each task
leaves the tree green: a richer-but-unread marker (T1), an unused policy reader
(T2), unread context fields (T3), accepted-but-unused config keys (T8), a core
undo with no second caller yet (T4), a defined-but-unregistered handler (T6), and
a status section that reads pending/n/a until a pass runs (T9) are all inert
until T5 (reroute) and T7 (register + wire) connect them.
- T1: the claude
_hypawaremarker and codex marked block each carry a complete undo record (claude:prev_base_url+ managed hook entries; codex: priormodel_provider); an idempotent re-attach keeps the originalprev_base_url, not the gateway URL. - T2:
readAttachPolicytri-state, absent block → default-on (onJoin: undefined),on_join: false→ opt-out, present-but-malformed → fail-safe opt-out (mirroringbackfill_policy.js). - T3: type-only; covered by the type-check / build and by T6's handler tests reading the new context fields.
- T4: disk-driven reverse with no adapter loaded, given a fixture
settings file with a hypaware marker, the generic undo strips the managed
keys/hooks/block, restores
prev_base_url/ priormodel_provider, and leaves no orphanedhyp claude-hookentries. A pre-existing foreign base URL round-trips byte-for-byte; the no-pre-existing-URL fixture round-trips to empty. - T5: one undo, both call sites, the core undo is exercised by manual
hyp detachand (in T6) the reconcilerreverse()against the same fixtures and reaches the same end-state; theclaude_attach_detach/client_attach_idempotentsmokes stay green. - T6: reverse gap, a
desired()that drops a previously-applied client triggersreverse()once (invoking the T4 undo), the marker is removed, and a second pass is a no-op; a throwingattach()writes afailedmarker that retries with bumpedattempts; daemon-only, with no gateway capabilityclients/endpointare undefined and the handler is inert. - T7: the daemon resolves
clientDescriptors/clients/endpointand registers[attachHandler, backfillHandler](attach first); a boot already-confirmed pass and a confirm-edge pass both reach the handler. - T8: the claude/codex section validator accepts
{ on_join }and rejects a non-boolean; a central-lockedattachentry cannot be flipped by a colliding local entry (reuse the LLP 0031 merge-drop harness). - T9: mixed
done/failed/pending/n/areads cleanly;attach.on_join: falseand a non-joined host both rendern/a; afailedattach does not flipoveralltodegraded. - End-to-end (hermetic smoke, lands with T7): a seeded join confirming a
config that names
@hypaware/claudeauto-attaches (settings written, markerdone, status attached) and does not re-attach on a second confirmed poll; then a follow-up confirmed config that drops@hypaware/claudereverses it (settings restored), the Part 5 config-drop trigger, exercised post-restart.
- id: T1 branch: task/client-attach/T1 deps: [] -- attach() writes a self-describing undo record: claude
_hypawaremarker (claude/src/settings.js MARKER_KEY, via writeAttachOutput/anthropic.js) records prev_base_url + managed ANTHROPIC_BASE_URL/SessionStart hook entries; codex# BEGIN/END hypawaremarked block (codex/src/settings.js) records prior model_provider. Unit-tested via marker contents. - id: T2 branch: task/client-attach/T2 deps: [] -- Attach policy reader: new src/core/config/attach_policy.js (readAttachPolicy, tri-state over config.attach.on_join), the backfill_policy.js twin shared by the handler (T6) and status (T9). Pure; unit-tested.
- id: T3 branch: task/client-attach/T3 deps: [] -- Reconcile-context seam: extend ActionContext + ReconcileInput in src/core/config/types.d.ts with optional clientDescriptors (Map<string, ClientDescriptor>), clients (AiGatewayCapability), endpoint (string). Type-only; no behaviour change until a handler reads them.
- id: T8 branch: task/client-attach/T8 deps: [] -- Per-plugin attach config validation: validateAttachSection beside validateBackfillSection in the claude/codex config.js, wired into validateClaudeConfig and the codex equivalent. Validates config.attach.on_join; plugin-local, no core schema change.
- id: T4 branch: task/client-attach/T4 deps: [T1] -- The single core undo (= detach): new src/core/config/client_detach_disk.js reversing a client's attach from the attachProbe descriptor + the marker undo record, format-aware (json marker-key / toml managed-block) but plugin-agnostic, reusing resolveClientSettingsPath (client_settings_path.js) and the probeClientAttached format logic (status.js). Subsumes the adapters' old detach() incl. the Codex marked-block strip + model_provider restore. Unit-tested on fixtures with no plugin loaded.
- id: T5 branch: task/client-attach/T5 deps: [T4] -- Retire adapter detach() + reroute manual detach: drop detach from AiGatewayClientRegistration (hypaware-plugin-kernel-types.d.ts) and from the claude/codex registerClient() calls, and point runClientLifecycle's detach branch (src/core/cli/core_commands.js) at the T4 core undo via the clientDescriptor. The claude_attach_detach / client_attach_idempotent smokes must stay green.
- id: T6 branch: task/client-attach/T6 deps: [T2, T3, T4] -- Attach handler: new src/core/config/action_attach.js (createAttachHandler + attachHandler) mirroring action_backfill.js. desired() over ctx.clientDescriptors ∩ enabled plugins ∩ readAttachPolicy, guarded on ctx.clients.getClient(name); perform() calls attach({ endpoint: ctx.endpoint, json:true }), parses the one-line JSON, records detail (settings_path, prev_value); reverse() invokes the T4 disk-driven undo (not ctx.clients). Unit-tested with injected fake clientDescriptors + clients + fs.
- id: T7 branch: task/client-attach/T7 deps: [T3, T6] -- Daemon wiring in src/core/daemon/runtime.js: resolve clientDescriptors (plugin catalog), clients/endpoint (boot.runtime.capabilities when the gateway is enabled, gateway.localEndpoint() with the configuredGatewayEndpoint fallback), thread onto the reconcile context, and register handlers [attachHandler, backfillHandler], attach first. Lands the end-to-end hermetic auto-attach/reverse smoke.
- id: T9 branch: task/client-attach/T9 deps: [T2] -- Status surface: buildClientActionsReport (src/core/daemon/status.js) gains a declared-attach-targets derivation symmetric to backfill's, reusing the clientDescriptors-derived plugin set (backfillPlugins) and readAttachPolicy: enabled client plugin on a joined host -> pending (no marker) / n-a (on_join:false or non-joined) / attached (done marker); must not flip overall to degraded.
- id: T10 branch: task/client-attach/T10 deps: [T1, T2, T3, T4, T5, T6, T7, T8, T9] -- flip LLP 0045 Status Accepted->Active (shipped marker, LLP 0016).
- LLP 0045: the implementation design this plan schedules
- LLP 0044: client attach on join (the decision)
- LLP 0041, the seam + backfill design this mirrors (sibling plan: LLP 0043)
- LLP 0036: the action seam (reversible-handler contract)
- LLP 0016: AI gateway / client adapters (
registerClient,attach,localEndpoint) src/core/config/action_reconciler.js,src/core/config/action_backfill.js,src/core/config/backfill_policy.js,src/core/daemon/runtime.js,src/core/daemon/status.js,src/core/daemon/client_settings_path.js,src/core/cli/core_commands.js,src/core/plugin_catalog.js: the code these tasks build on