From 69726ea131381c52a261cad61f14cf4fe9f2353e Mon Sep 17 00:00:00 2001 From: test Date: Mon, 17 Aug 2026 20:37:24 +0000 Subject: [PATCH 1/6] LLP 0246: proxy-mode default and attach migration design covers issue 0242 Design of record for LLP 0242's fix (LLP 0243/0244): the picker fold and hyp init claude preset compose proxy_mode: true by default, and hyp attach claude migrates existing base-URL installs behind an explicit consent prompt via enableGatewayProxyMode. Co-Authored-By: Claude Opus 5 (1M context) --- llp/0246-proxy-mode-default-attach.design.md | 168 +++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 llp/0246-proxy-mode-default-attach.design.md diff --git a/llp/0246-proxy-mode-default-attach.design.md b/llp/0246-proxy-mode-default-attach.design.md new file mode 100644 index 00000000..09cacafd --- /dev/null +++ b/llp/0246-proxy-mode-default-attach.design.md @@ -0,0 +1,168 @@ +# LLP 0246: Proxy mode by default and the attach migration, technical design + +**Type:** design +**Status:** Active +**Systems:** Config, Plugins, Gateway, Daemon +**Generated-by:** neutral +**Depends-on:** proxy-mode-capture +**Related:** LLP 0242, LLP 0243, LLP 0244, LLP 0231, LLP 0232, LLP 0233, +LLP 0031, LLP 0174, LLP 0181, LLP 0183 + +> Technical design for closing LLP 0242: every config-writing install path +> composes `proxy_mode: true` when a picked client attaches by proxy, and +> `hyp attach claude` becomes the consented migration verb for existing +> base-URL installs. Named files, the compose and write seams, consent and +> back-compat rules, failure modes, and what the tests prove. + +Coverage anchor: + +`@ref LLP 0242: fresh installs still attached Claude by base URL and old installs had no path to proxy mode; the picker-fold gateway_proxy_mode composition, the hyp init claude literal key, and the attach-time consented migration through enableGatewayProxyMode this document designs are LLP 0242's fix` + +## 0. Scope and code status {#scope} + +LLP 0242 is the accepted issue; LLP 0243 (composition) and LLP 0244 +(migration) are its resolution decisions. This document is the +implementation design binding them to the tree; it builds directly on the +proxy-mode capture stack designed in LLP 0245 (the `proxy-mode-capture` +change set), reusing its `waitForLocalCa` seam and the LLP 0232 attach +preflight semantics. + +The design is realized on `master` by commit `04330abb` (#794). Paths and +names below are verified against that tree; the tests in section 5 exist +and gate it. This document is the request-level design of record, with the +request (LLP 0242) `@ref`'d above. + +## 1. Why nothing turned the key on {#why} + +LLP 0233 made `proxy_mode` explicit: only the literal config key enables +interception, never inference, upgrade, or adapter installation. The +gateway mints the CA only when the key is on, and the LLP 0232 attach +preflight picks proxy mode only when that CA exists on disk. Correct at +every link, and no link ever started the chain: neither the picker +composer, the express path, nor the `hyp init claude` preset wrote the +key, so every install landed on base-URL attach. The fix therefore has +exactly two halves: who writes the key for a fresh config (LLP 0243), and +who writes it into a config that already exists (LLP 0244). + +## 2. Fresh installs: composition writes the key {#composition} + +**Manifest declaration.** A picker row whose client attaches by proxy +declares it in its plugin manifest, beside the upstream it already +contributes: `hypaware-core/plugins-workspace/claude/hypaware.plugin.json` +carries `"gateway_proxy_mode": true` in its picker row's `compose` block. +The Codex row does not declare it, so a Codex-only install mints no CA it +will never use (LLP 0243 #composed-default). + +**The fold.** `composePickerConfig` in `src/core/cli/walkthrough.js` +accumulates `gatewayProxyMode` across picked descriptors +(`if (compose.gateway_proxy_mode === true) gatewayProxyMode = true`) the +same way it accumulates `requires_gateway` and upstream contributions. +When set, the composed `@hypaware/ai-gateway` entry carries the explicit +`proxy_mode: true` key in the written file, preserving LLP 0233's +key-is-the-only-switch invariant: composition writes whole configs on the +user's instruction, which is not inference. Every path that rides the fold +(interactive picker, express, `hyp init --yes`) produces a proxy-mode +config by default. No `listen` is written, keeping the LLP 0114 +default-only EADDRINUSE fallback. + +**The literal preset.** `hyp init claude` +(`hypaware-core/plugins-workspace/claude/src/index.js`) writes its config +literally rather than through the fold, so it writes +`proxy_mode: true` literally on its gateway entry. + +**The user's key wins on reconfigure.** The carry-forward merge (LLP 0183) +lets a prior gateway entry own the key entirely on the interactive +reconfigure lane, the only lane that supplies `args.existing`: a +hand-written `proxy_mode: false` survives, and so does the key's +*absence*, because a reconfigure is a picker run, not the migration verb. +Non-interactive re-init (`--yes`, presets, `--from-file`) composes from +scratch by design; it only overwrites behind an explicit `--force`, which +is the whole-file consent, and so re-applies the composed default like a +fresh install (LLP 0243 #user-key-wins). + +**Finale ordering.** The walkthrough's attach lane runs against the daemon +it just started, so before attaching a proxy-mode client the finale waits +on `waitForLocalCa` (imported from `src/core/tls/ca.js` into +`walkthrough.js`); otherwise the LLP 0232 preflight would race the +gateway's first CA mint and silently produce a base-URL attach. + +## 3. Existing installs: attach is the migration verb {#migration} + +**The offer.** `maybeOfferProxyModeMigration({ name, ctx, parsed })` in +`src/core/commands/clients.js` runs before the gateway endpoint is +resolved, so an accepted migration restarts the daemon first and the +endpoint resolution then discovers the fresh one. It offers only when the +client's picker descriptor declares `compose.gateway_proxy_mode: true` +and the effective gateway block lacks `proxy_mode: true`. The offer is +keyed on the *config*, not the CA: a stale CA with the key off means an +earlier install was half-unwound, and the config write is still the +repair. One yes/no question, default no, names the three consequences: the +config write, the daemon restart, and the coming macOS trust dialog. +Consent here covers the config write only; the CA trust grant keeps its +own dialog (LLP 0237) and can be refused independently (LLP 0244 +#attach-offers). + +**Consent boundaries.** A dry run says nothing. Non-TTY, `--json`, and +`hyp attach all` never prompt and never migrate; each attaches exactly as +today and emits the one stderr line naming the interactive command that +migrates (LLP 0244 #non-interactive). When the gateway block comes from +the central layer, a local write would be dropped as a collision +(LLP 0031 merge), so attach reports that proxy mode is fleet-managed and +where to enable it, in every attach shape (LLP 0244 #central-managed). +The helper never throws into the attach: the caller downgrades any escape +to a warning, because base-URL attach is what the install already does and +remains the working fallback. + +**The write.** `enableGatewayProxyMode(args)` in +`src/core/config/gateway_proxy_enable.js` reuses the LLP 0174 enable +machinery with one new write shape: it sets `proxy_mode: true` on the +*existing* local `@hypaware/ai-gateway` entry (guarded local write with +the LLP 0031 backup) rather than appending a plugin, and refuses with +outcome `no_gateway` when no layer provides one, because inventing a +gateway entry is not this verb's job. It then restarts the daemon, waits +for the bind, and waits for the CA file via the injectable `waitForCaFn` +(defaulting to `waitForLocalCa`), because attaching before the gateway +has minted the CA would silently produce another base-URL attach +(LLP 0244 #enable-write). Every step is reported; the seams +(`daemonStatus`, `restartDaemon`, `waitForBind`, `waitForCaFn`, `sleep`, +`now`) are injectable so tests never drive the real service manager +(LLP 0181). When no daemon service is installed, the write still lands +and the output says the daemon start is the remaining step. + +**Idempotence.** Once the key is set the offer never appears again; the +attach proceeds straight into the LLP 0245 proxy attach. + +## 4. Failure modes {#failure-modes} + +- **Migration accepted but the daemon restart or bind fails**: reported + per step; the attach continues in base-URL mode with a warning naming + what failed. The config key is written, so the next daemon start and the + next attach complete the switch. +- **CA never appears within `caTimeoutMs`**: same downgrade; the gateway's + own status (`proxy_mode_error`) says why interception did not come up. +- **No gateway entry in any layer**: `no_gateway` refusal; a config with + no gateway anywhere has a bigger problem than proxy mode. +- **Fleet-managed gateway**: no local write ever; report-only, so the + local CLI never fights the central layer. +- **Declined default**: a hand-written `proxy_mode: false` (or its + absence, on the interactive reconfigure lane) is permanent until the + user acts; no path re-adds the key by inference. + +## 5. What the tests prove {#tests} + +Traditional (root `test/`): `test/core/init-proxy-mode-default.test.js` +(every fold-riding install path and the literal preset write the key; the +Codex-only pick does not; the carry-forward lets a prior entry own the +key), `test/core/attach-proxy-migration.test.js` (the offer appears +exactly for a proxy-capable client on a key-less config; default no; +dry-run silence; non-TTY, `--json` and `attach all` pointer lines; +central-managed report; failure downgraded to warning), +`test/core/gateway-proxy-enable.test.js` (guarded write on the existing +entry, `no_gateway` refusal, restart/bind/CA waits through injected +seams), `test/core/walkthrough-finale-ca-wait.test.js` (the finale waits +for the CA before attaching), and `test/core/walkthrough-attach-lane.test.js` +(attach lane ordering). Per the LLP 0244 consequence, everything above +runs with the `security` / `launchctl` seams refused or shimmed; hermetic +smokes (`walkthrough_picker_to_first_query`, `claude_attach_detach`) boot +gateways that mint a CA only inside the temp `HYP_HOME` and must never +reach the host keychain or launchd table. From c02d7e058eef435431560973acc9d0d45ecbe04b Mon Sep 17 00:00:00 2001 From: test Date: Mon, 17 Aug 2026 21:02:22 +0000 Subject: [PATCH 2/6] LLP 0249: plan for proxy-mode default and attach migration covers design 0246 The design (LLP 0246) is already realized on master by 04330abb (#794); the plan records the symbol-by-symbol conformance pass, enumerates the nine consent/refusal pins that exist, and schedules the two outstanding items: the missing --json non-interactive test pin (T1) and the corpus cross-links binding 0242/0243/0244 to the design of record (T2). Co-Authored-By: Claude Opus 5 (1M context) --- llp/0249-proxy-mode-default-attach.plan.md | 193 +++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 llp/0249-proxy-mode-default-attach.plan.md diff --git a/llp/0249-proxy-mode-default-attach.plan.md b/llp/0249-proxy-mode-default-attach.plan.md new file mode 100644 index 00000000..fc717e2a --- /dev/null +++ b/llp/0249-proxy-mode-default-attach.plan.md @@ -0,0 +1,193 @@ +# LLP 0249: Proxy mode by default and the attach migration, implementation plan + +**Type:** plan +**Status:** Active +**Related:** LLP 0246, LLP 0242 +**Generated-by:** neutral + +> [LLP 0246](./0246-proxy-mode-default-attach.design.md) is the technical +> design closing LLP 0242. This plan's central finding, verified against the +> tree rather than assumed from the design's prose: the design is already +> realized on `master` by commit `04330abb` (#794), exactly as its section 0 +> states. Every file, symbol, test, `@ref` annotation, and smoke edit the +> design names exists and the section 5 gate passes (32/32 across the five +> named test files on this branch). This plan therefore schedules no rebuild +> of working code. It schedules the two things a symbol-by-symbol conformance +> pass actually found outstanding: one missing test pin (the `--json` attach +> shape the design claims is proven but no test exercises) and the corpus +> cross-links binding the design of record to the docs it covers. + +## What is already built on master (verified, not planned) + +Each design section was checked against the tree at `04330abb`, which is an +ancestor of this integration branch. Nothing below is a task; it is the +record of why it is not one. + +**Section 2, fresh installs (LLP 0243), fully built:** + +- `hypaware-core/plugins-workspace/claude/hypaware.plugin.json` line 45 + declares `"gateway_proxy_mode": true` in the claude picker row's `compose` + block. The codex manifest declares nothing, so a Codex-only install mints + no CA, as designed. +- `composePickerConfig` in `src/core/cli/walkthrough.js` folds the flag + (line 973) and writes `proxy_mode: true` onto the composed + `@hypaware/ai-gateway` entry (line 1014), `@ref`'d to + LLP 0243#composed-default. No `listen` is written. +- The `hyp init claude` literal preset + (`hypaware-core/plugins-workspace/claude/src/index.js` line 413) writes + the key literally, `@ref`'d to the same anchor. +- The carry-forward merge lets a prior gateway entry own the key entirely, + including its absence (`walkthrough.js` line 1276, `@ref` LLP + 0243#user-key-wins), pinned by `test/core/compose-picker-config.test.js` + lines 514 and 534. +- The finale waits on `waitForLocalCa` (imported from `src/core/tls/ca.js`, + wait seam at `walkthrough.js` line 1537) before a proxy-mode attach, + pinned by `test/core/walkthrough-finale-ca-wait.test.js`. + +**Section 3, existing installs (LLP 0244), fully built:** + +- `maybeOfferProxyModeMigration` (`src/core/commands/clients.js` line 821) + runs before endpoint resolution (line 308), never throws into the attach + (the caller downgrades to a warning), and gates exactly as designed: + dry-run silent; `attach all`, `--json`, and non-TTY emit the one pointer + line (line 886); a central-layer gateway reports fleet management instead + of prompting; the offer is keyed on the config, not the CA. +- `enableGatewayProxyMode` (`src/core/config/gateway_proxy_enable.js`) sets + the key on the existing local entry with the LLP 0031 guarded write, + refuses `no_gateway` and `central_managed` (line 154), restarts, waits for + bind and CA through injectable seams (`waitForCaFn` defaulting to + `waitForLocalCa`, lines 80 and 251), and reports every step. +- `@ref` annotations to LLP 0243/0244 anchors are present in + `walkthrough.js`, `clients.js`, `gateway_proxy_enable.js`, the claude + preset, and the tests. All six anchors the design and code cite + (`#composed-default`, `#user-key-wins`, `#attach-offers`, + `#non-interactive`, `#central-managed`, `#enable-write`) resolve in + LLP 0243/0244. LLP 0233 already carries its `Extended-by: LLP 0244` + forward ref (landed in #794). + +**Section 5, the gate, built and green on this branch:** + +`test/core/init-proxy-mode-default.test.js`, +`test/core/attach-proxy-migration.test.js`, +`test/core/gateway-proxy-enable.test.js`, +`test/core/walkthrough-finale-ca-wait.test.js`, and +`test/core/walkthrough-attach-lane.test.js` all exist and pass (32/32 on +this branch). Both named smokes were updated by #794: +`walkthrough_picker_to_first_query.js` asserts `proxy_mode: true` in the +golden composed config, and `claude_attach_detach.js` asserts the non-TTY +pointer note is the attach's only stderr line. + +### The consent and refusal pins, enumerated + +The consented-migration path is the delicate part of this design, so this +plan records exactly which behaviours `test/core/attach-proxy-migration.test.js` +already pins, one test each: + +1. Decline: the question is asked once, default no, nothing is written, the + attach still lands, and the re-run pointer is printed. +2. Accept: `proxy_mode: true` lands in the local config; with no daemon + service installed the output names the daemon start as the remaining step. +3. Idempotence: with the key already set, no question and no note. +4. A client whose picker row does not declare `compose.gateway_proxy_mode` + is never asked. +5. Central-managed gateway: no question even on a TTY, the fleet-managed + note instead, nothing written, even beside a local entry. +6. Non-TTY: no question, exactly the one pointer line, attach unchanged + (also pinned from the enablement side in + `test/core/attach-enablement-state.test.js` line 286 and end-to-end in + the `claude_attach_detach` smoke). +7. `--dry-run`: no question, no note, no write. +8. `hyp attach all`: never asks mid-run, points at the interactive command. +9. A failed accepted migration warns and the attach still succeeds in + base-URL mode. + +**The one drift found:** design section 5 claims the tests prove the +"non-TTY, `--json` and `attach all` pointer lines", but no test anywhere +passes `json: true`. The code gates on `parsed.json` correctly +(`clients.js` line 886), and the test harness already plumbs an unused +`json` opt (`attach-proxy-migration.test.js` line 76), so only the pin is +missing: a `--json` run on a TTY is the one shape whose no-prompt guarantee +nothing enforces against regression. T1 closes this. + +## What remains: the task rationale + +- **T1** adds the missing `--json` pin. Small and mechanical because the + harness support already exists; it matters because `--json` on a TTY is + the only LLP 0244 #non-interactive shape a regression could silently + re-prompt, and a prompt inside a `--json` run would both hang scripted + callers and corrupt the JSON contract. +- **T2** lands the corpus cross-links: LLP 0242, 0243, and 0244 predate the + design of record and none of their `Related:` lines names LLP 0246. + Adding the forward link is a trivial editorial fix, which the repo's LLP + conventions explicitly permit on Accepted docs. T2 also re-verifies the + six anchors and runs `/ref-check` over the files this change set touches, + so the corpus lands coherent. + +The two tasks touch disjoint files (one test file; three llp docs) and run +fully in parallel. + +Any further drift a task worker finds between LLP 0246's prose and the tree +must become a new issue LLP, not an edit: LLP 0246 is Active and settled, +and this plan's verification already establishes the tree matches it +everywhere checked. + +## External blockers (not expressible as `deps`) + +- **Merge order: `integration/proxy-mode-capture` must land first.** The + design declares `Depends-on: proxy-mode-capture` and its section 0 cites + LLP 0245, which exists only on that branch; `llp/0245-*.design.md` is + absent from this branch. The *code* dependency is already satisfied + (master carries the proxy-mode capture stack via #782 and #792), so + neither task here is blocked; only the doc reference dangles until that + branch merges. Merging this branch first would leave LLP 0246 citing a + document the tree does not yet have. +- **Pre-existing unrelated failures.** The full `npm test` run on this + branch shows a failing cluster confined to parquet/iceberg NULL semantics, + pushdown conversion, and report rendering, none of it touched by this + change set; #794's own commit message records the same cluster as + pre-existing on a clean tree. Task workers should gate on the section 5 + test files plus the files they touch, not on that cluster turning green. + +## Assumption stated for the record + +This change set's design was written retrospectively against work that +merged to master as #794 before the design doc itself landed. This plan +takes the neutral pipeline's requirement (no plan means nothing can be +implemented) at face value and supplies the plan as the record binding the +change set together: verification of the as-built tree, plus the two +genuinely outstanding items as tasks. If a human would rather close the +change set with no tasks at all, T2 should still land somewhere; the +corpus cross-links otherwise never get written. + +## Notes for implementers + +- JavaScript, no semicolons; no em dashes anywhere, including in test names + and llp doc edits. +- T1's new tests ride the existing harness in + `test/core/attach-proxy-migration.test.js` (its `runAttach` opts already + accept `json`); no new seams are needed. Everything runs with the + `security` / `launchctl` seams refused or shimmed, per the LLP 0244 + consequence the existing tests in that file already honor. +- T2's edits are one line per doc on the `Related:` metadata line; do not + touch any settled section body. Run `/ref-check llp/` (or the repo's + ref-check skill against the touched files) before the PR. +- Neither task flips any LLP status. LLP 0242/0243/0244 stay Accepted, + LLP 0246 stays Active. + +## References + +- [LLP 0246](./0246-proxy-mode-default-attach.design.md): the design this + plan schedules, section by section +- [LLP 0242](./0242-fresh-installs-attach-base-url.issue.md): the request; + [LLP 0243](./0243-picker-composes-proxy-mode.decision.md) and + [LLP 0244](./0244-attach-migrates-to-proxy-mode.decision.md): its + resolution decisions, both fully realized by `04330abb` (#794) +- LLP 0245 (`integration/proxy-mode-capture`): the predecessor design; + see External blockers for the merge-order constraint +- `llp/0173-openclaw-two-lane-capture.plan.md`: format precedent for the + task-graph and complexity-rating structure + +## Tasks + +- id: T1 branch: task/proxy-mode-default-attach/T1 deps: [] complexity: 2 -- test/core/attach-proxy-migration.test.js: add the missing LLP 0244 #non-interactive pin for the `--json` attach shape. Using the file's existing harness (its opts already plumb `json`, line 76, currently unused), add a test running `hyp attach --client claude --json` on a TTY against a proxy-capable descriptor and a key-less local gateway config, asserting: no prompt is issued (the askYesNo seam is never reached), stderr carries exactly the one pointer line `note: this install attaches claude by base URL; run 'hyp attach claude' in an interactive terminal to switch it to proxy mode`, no config write occurs (`proxy_mode` stays absent from the file on disk), and stdout remains the attach's valid JSON payload with nothing interleaved. Add a companion case proving `--json` combined with non-TTY still emits the pointer exactly once, not twice. Annotate with `// @ref LLP 0244#non-interactive [tests]: --json never prompts even on a TTY and emits exactly the pointer`. Gate: this file plus test/core/attach-enablement-state.test.js pass. No production code change is expected; if the assertion fails against `src/core/commands/clients.js` line 886's `parsed.json` gate, that is a bug to report, not a test to soften. +- id: T2 branch: task/proxy-mode-default-attach/T2 deps: [] complexity: 1 -- Corpus cross-links for the design of record: append `LLP 0246` to the `Related:` metadata line of llp/0242-fresh-installs-attach-base-url.issue.md, llp/0243-picker-composes-proxy-mode.decision.md, and llp/0244-attach-migrates-to-proxy-mode.decision.md (trivial editorial forward-refs, permitted on Accepted docs; touch only the metadata line, never a settled section body). Re-verify that the six anchors LLP 0246 and the code cite resolve: #composed-default and #user-key-wins in 0243, #attach-offers, #enable-write, #central-managed, and #non-interactive in 0244. Run the repo's ref-check skill over llp/ and the files carrying `@ref LLP 0243` / `@ref LLP 0244` annotations (src/core/cli/walkthrough.js, src/core/commands/clients.js, src/core/config/gateway_proxy_enable.js, hypaware-core/plugins-workspace/claude/src/index.js, the test files) and confirm zero broken references. Gate: ref-check clean; no test changes. From 38a0e39661388fe96ba7747be1534fa208e562ba Mon Sep 17 00:00:00 2001 From: test Date: Mon, 17 Aug 2026 21:12:37 +0000 Subject: [PATCH 3/6] llp: cross-link LLP 0246 into 0242, 0243, 0244 Related lines The design of record for the proxy-mode-default-attach change set (LLP 0246) landed after the request and decision docs it closes. Append it to the Related metadata line of LLP 0242 (issue), LLP 0243 and LLP 0244 (decisions) so the corpus forward-links to the design, per LLP 0156's allowance for trivial editorial forward-refs on Accepted docs. Re-verified all six anchors LLP 0246 and the annotated code cite (#composed-default, #user-key-wins in 0243; #attach-offers, #enable-write, #central-managed, #non-interactive in 0244) resolve, and ran ref-check over llp/ plus every file carrying an @ref LLP 0243/0244 annotation: zero broken references. Task-Id: T2 --- llp/0242-fresh-installs-attach-base-url.issue.md | 2 +- llp/0243-picker-composes-proxy-mode.decision.md | 2 +- llp/0244-attach-migrates-to-proxy-mode.decision.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/llp/0242-fresh-installs-attach-base-url.issue.md b/llp/0242-fresh-installs-attach-base-url.issue.md index f0ee2d2b..916be703 100644 --- a/llp/0242-fresh-installs-attach-base-url.issue.md +++ b/llp/0242-fresh-installs-attach-base-url.issue.md @@ -5,7 +5,7 @@ **Systems:** Config, Plugins, Gateway **Author:** Phil / Claude **Date:** 2026-08-17 -**Related:** LLP 0231, LLP 0232, LLP 0233, LLP 0174 +**Related:** LLP 0231, LLP 0232, LLP 0233, LLP 0174, LLP 0246 > Proxy-mode capture shipped (LLP 0231-0239) but nothing writes > `proxy_mode: true`, so every install path still lands on the base-URL diff --git a/llp/0243-picker-composes-proxy-mode.decision.md b/llp/0243-picker-composes-proxy-mode.decision.md index d88cff6e..ea7270e6 100644 --- a/llp/0243-picker-composes-proxy-mode.decision.md +++ b/llp/0243-picker-composes-proxy-mode.decision.md @@ -5,7 +5,7 @@ **Systems:** Config, Plugins, Gateway **Author:** Phil / Claude **Date:** 2026-08-17 -**Related:** LLP 0130, LLP 0135, LLP 0213, LLP 0232, LLP 0242 +**Related:** LLP 0130, LLP 0135, LLP 0213, LLP 0232, LLP 0242, LLP 0246 **Extends:** LLP 0233 (#proxy-mode-is-explicit: the key is still the only switch and is still explicit in the file; what changes is who writes it) diff --git a/llp/0244-attach-migrates-to-proxy-mode.decision.md b/llp/0244-attach-migrates-to-proxy-mode.decision.md index 955da846..dfd4ef3a 100644 --- a/llp/0244-attach-migrates-to-proxy-mode.decision.md +++ b/llp/0244-attach-migrates-to-proxy-mode.decision.md @@ -6,7 +6,7 @@ **Author:** Phil / Claude **Date:** 2026-08-17 **Related:** LLP 0031, LLP 0174, LLP 0181, LLP 0232, LLP 0233, LLP 0242, -LLP 0243 +LLP 0243, LLP 0246 > When `hyp attach claude` runs against an effective config whose gateway > block lacks `proxy_mode: true`, and the client's row declares proxy attach, From 839e49612ca78330800e2fa00382f741b8b77e76 Mon Sep 17 00:00:00 2001 From: test Date: Mon, 17 Aug 2026 21:28:45 +0000 Subject: [PATCH 4/6] T1: pin --json attach never prompts even on a TTY (LLP 0244 #non-interactive) test/core/attach-proxy-migration.test.js was missing coverage for the --json attach shape: the code gates on parsed.json (clients.js line 886) but no test exercised it. Add two cases riding the existing harness (its json opt was plumbed but unused): --json on a TTY, and --json combined with non-TTY, both asserting the askYesNo seam is never reached, stderr carries exactly the one pointer line, no config write occurs, and stdout stays the attach's valid JSON payload with nothing interleaved. Task-Id: T1 --- test/core/attach-proxy-migration.test.js | 72 +++++++++++++++++++++++- 1 file changed, 70 insertions(+), 2 deletions(-) diff --git a/test/core/attach-proxy-migration.test.js b/test/core/attach-proxy-migration.test.js index cdd5fd57..e89a6395 100644 --- a/test/core/attach-proxy-migration.test.js +++ b/test/core/attach-proxy-migration.test.js @@ -75,7 +75,7 @@ function writeGatewayConfig(home, opts) { * * @param {{ home: string, answer?: string, tty?: boolean, json?: boolean }} opts */ -function makeCtx({ home, answer, tty = true }) { +function makeCtx({ home, answer, tty = true, json = false }) { const registered = ['claude', 'codex'] const gateway = { localEndpoint() { @@ -86,12 +86,20 @@ function makeCtx({ home, answer, tty = true }) { if (!registered.includes(name)) return undefined return { name, - /** @param {{ endpoint: string, dryRun?: boolean }} args */ + /** @param {{ endpoint: string, dryRun?: boolean, stdout: { write(chunk: unknown): boolean } }} args */ async attach(args) { writeFileSync( path.join(home, `${name}-attached.json`), JSON.stringify({ endpoint: args.endpoint, dryRun: args.dryRun === true }) ) + // Mirror a real adapter's --json contract: under json, stdout carries + // exactly the one-line machine payload and nothing else, so the + // --json pins below can assert it stays clean of any migration note. + if (json) { + args.stdout.write( + JSON.stringify({ status: 'ok', action: 'attach', client: name, dry_run: args.dryRun === true }) + '\n' + ) + } }, } }, @@ -229,6 +237,66 @@ test('non-TTY: no question, one pointer note, attach unchanged', async () => { }) }) +// @ref LLP 0244#non-interactive [tests]: --json never prompts even on a TTY and emits exactly the pointer +test('--json on a TTY: no prompt, exactly one pointer note, no write, stdout stays the attach JSON payload', async () => { + await withTempHome(async (home) => { + writeGatewayConfig(home) + const before = readFileSync(localConfigPath(home), 'utf8') + // No `answer` is queued: if the askYesNo seam were somehow reached, the + // prompt would hang reading from an empty stdin, so this also guards + // against the seam being reached in a way a text assertion alone would + // miss. + const { ctx, stdout, stderr } = makeCtx({ home, tty: true, json: true }) + const code = await runAttach(['--client', 'claude', '--json'], ctx) + assert.equal(code, 0, stderr.text()) + // The askYesNo seam is never reached: its question never reaches stderr. + assert.ok(!stderr.text().includes(MIGRATION_QUESTION)) + // Exactly the one pointer line, nothing else on stderr. + assert.equal( + stderr.text(), + "note: this install attaches claude by base URL; run 'hyp attach claude' in an " + + 'interactive terminal to switch it to proxy mode\n' + ) + // No config write: proxy_mode stays absent from the file on disk. + const after = readFileSync(localConfigPath(home), 'utf8') + assert.equal(after, before) + assert.doesNotMatch(after, /proxy_mode/) + // stdout stays the attach's valid JSON payload, nothing interleaved. + const stdoutText = stdout.text() + assert.equal(stdoutText.split('\n').filter((line) => line.length > 0).length, 1) + const payload = JSON.parse(stdoutText) + assert.equal(payload.status, 'ok') + assert.equal(payload.client, 'claude') + }) +}) + +// @ref LLP 0244#non-interactive [tests]: --json never prompts even on a TTY and emits exactly the pointer +test('--json combined with non-TTY still emits the pointer exactly once, not twice', async () => { + await withTempHome(async (home) => { + writeGatewayConfig(home) + const before = readFileSync(localConfigPath(home), 'utf8') + const { ctx, stdout, stderr } = makeCtx({ home, tty: false, json: true }) + const code = await runAttach(['--client', 'claude', '--json'], ctx) + assert.equal(code, 0, stderr.text()) + assert.ok(!stderr.text().includes(MIGRATION_QUESTION)) + // Both conditions (json and non-TTY) independently qualify for the + // pointer; it must still land exactly once, never doubled. + assert.equal( + stderr.text(), + "note: this install attaches claude by base URL; run 'hyp attach claude' in an " + + 'interactive terminal to switch it to proxy mode\n' + ) + const after = readFileSync(localConfigPath(home), 'utf8') + assert.equal(after, before) + assert.doesNotMatch(after, /proxy_mode/) + const stdoutText = stdout.text() + assert.equal(stdoutText.split('\n').filter((line) => line.length > 0).length, 1) + const payload = JSON.parse(stdoutText) + assert.equal(payload.status, 'ok') + assert.equal(payload.client, 'claude') + }) +}) + test('--dry-run: no question, no note, no write', async () => { await withTempHome(async (home) => { writeGatewayConfig(home) From 3efab8c2bde180457d0e33c1bcd861291de4129e Mon Sep 17 00:00:00 2001 From: test Date: Mon, 17 Aug 2026 21:33:52 +0000 Subject: [PATCH 5/6] LLP 0246 renumbered to 0251: master claimed 0246 for remote-control-absolute-form-requests The design was minted at 0246 while, concurrently, llp/0246-remote-control-absolute-form-requests.issue.md merged to master. Two live docs at one number produce an ambiguous @ref and a red ref-hygiene check once this branch merges, and neither branch's CI can see it. Per the repo's renumber rule the later claimant moves above the highest number claimed anywhere (0250), so the design becomes 0251. Reference sweep covers the plan (LLP 0249), the design's own title, and the three corpus cross-links T2 added to LLP 0242/0243/0244. No prose meaning changes. --- ...242-fresh-installs-attach-base-url.issue.md | 2 +- ...0243-picker-composes-proxy-mode.decision.md | 2 +- ...4-attach-migrates-to-proxy-mode.decision.md | 2 +- llp/0249-proxy-mode-default-attach.plan.md | 18 +++++++++--------- ...> 0251-proxy-mode-default-attach.design.md} | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) rename llp/{0246-proxy-mode-default-attach.design.md => 0251-proxy-mode-default-attach.design.md} (99%) diff --git a/llp/0242-fresh-installs-attach-base-url.issue.md b/llp/0242-fresh-installs-attach-base-url.issue.md index 916be703..396aba0e 100644 --- a/llp/0242-fresh-installs-attach-base-url.issue.md +++ b/llp/0242-fresh-installs-attach-base-url.issue.md @@ -5,7 +5,7 @@ **Systems:** Config, Plugins, Gateway **Author:** Phil / Claude **Date:** 2026-08-17 -**Related:** LLP 0231, LLP 0232, LLP 0233, LLP 0174, LLP 0246 +**Related:** LLP 0231, LLP 0232, LLP 0233, LLP 0174, LLP 0251 > Proxy-mode capture shipped (LLP 0231-0239) but nothing writes > `proxy_mode: true`, so every install path still lands on the base-URL diff --git a/llp/0243-picker-composes-proxy-mode.decision.md b/llp/0243-picker-composes-proxy-mode.decision.md index ea7270e6..d81e2d05 100644 --- a/llp/0243-picker-composes-proxy-mode.decision.md +++ b/llp/0243-picker-composes-proxy-mode.decision.md @@ -5,7 +5,7 @@ **Systems:** Config, Plugins, Gateway **Author:** Phil / Claude **Date:** 2026-08-17 -**Related:** LLP 0130, LLP 0135, LLP 0213, LLP 0232, LLP 0242, LLP 0246 +**Related:** LLP 0130, LLP 0135, LLP 0213, LLP 0232, LLP 0242, LLP 0251 **Extends:** LLP 0233 (#proxy-mode-is-explicit: the key is still the only switch and is still explicit in the file; what changes is who writes it) diff --git a/llp/0244-attach-migrates-to-proxy-mode.decision.md b/llp/0244-attach-migrates-to-proxy-mode.decision.md index dfd4ef3a..c4b7042f 100644 --- a/llp/0244-attach-migrates-to-proxy-mode.decision.md +++ b/llp/0244-attach-migrates-to-proxy-mode.decision.md @@ -6,7 +6,7 @@ **Author:** Phil / Claude **Date:** 2026-08-17 **Related:** LLP 0031, LLP 0174, LLP 0181, LLP 0232, LLP 0233, LLP 0242, -LLP 0243, LLP 0246 +LLP 0243, LLP 0251 > When `hyp attach claude` runs against an effective config whose gateway > block lacks `proxy_mode: true`, and the client's row declares proxy attach, diff --git a/llp/0249-proxy-mode-default-attach.plan.md b/llp/0249-proxy-mode-default-attach.plan.md index fc717e2a..72e54c7c 100644 --- a/llp/0249-proxy-mode-default-attach.plan.md +++ b/llp/0249-proxy-mode-default-attach.plan.md @@ -2,10 +2,10 @@ **Type:** plan **Status:** Active -**Related:** LLP 0246, LLP 0242 +**Related:** LLP 0251, LLP 0242 **Generated-by:** neutral -> [LLP 0246](./0246-proxy-mode-default-attach.design.md) is the technical +> [LLP 0251](./0251-proxy-mode-default-attach.design.md) is the technical > design closing LLP 0242. This plan's central finding, verified against the > tree rather than assumed from the design's prose: the design is already > realized on `master` by commit `04330abb` (#794), exactly as its section 0 @@ -117,7 +117,7 @@ nothing enforces against regression. T1 closes this. re-prompt, and a prompt inside a `--json` run would both hang scripted callers and corrupt the JSON contract. - **T2** lands the corpus cross-links: LLP 0242, 0243, and 0244 predate the - design of record and none of their `Related:` lines names LLP 0246. + design of record and none of their `Related:` lines names LLP 0251. Adding the forward link is a trivial editorial fix, which the repo's LLP conventions explicitly permit on Accepted docs. T2 also re-verifies the six anchors and runs `/ref-check` over the files this change set touches, @@ -126,8 +126,8 @@ nothing enforces against regression. T1 closes this. The two tasks touch disjoint files (one test file; three llp docs) and run fully in parallel. -Any further drift a task worker finds between LLP 0246's prose and the tree -must become a new issue LLP, not an edit: LLP 0246 is Active and settled, +Any further drift a task worker finds between LLP 0251's prose and the tree +must become a new issue LLP, not an edit: LLP 0251 is Active and settled, and this plan's verification already establishes the tree matches it everywhere checked. @@ -139,7 +139,7 @@ everywhere checked. absent from this branch. The *code* dependency is already satisfied (master carries the proxy-mode capture stack via #782 and #792), so neither task here is blocked; only the doc reference dangles until that - branch merges. Merging this branch first would leave LLP 0246 citing a + branch merges. Merging this branch first would leave LLP 0251 citing a document the tree does not yet have. - **Pre-existing unrelated failures.** The full `npm test` run on this branch shows a failing cluster confined to parquet/iceberg NULL semantics, @@ -172,11 +172,11 @@ corpus cross-links otherwise never get written. touch any settled section body. Run `/ref-check llp/` (or the repo's ref-check skill against the touched files) before the PR. - Neither task flips any LLP status. LLP 0242/0243/0244 stay Accepted, - LLP 0246 stays Active. + LLP 0251 stays Active. ## References -- [LLP 0246](./0246-proxy-mode-default-attach.design.md): the design this +- [LLP 0251](./0251-proxy-mode-default-attach.design.md): the design this plan schedules, section by section - [LLP 0242](./0242-fresh-installs-attach-base-url.issue.md): the request; [LLP 0243](./0243-picker-composes-proxy-mode.decision.md) and @@ -190,4 +190,4 @@ corpus cross-links otherwise never get written. ## Tasks - id: T1 branch: task/proxy-mode-default-attach/T1 deps: [] complexity: 2 -- test/core/attach-proxy-migration.test.js: add the missing LLP 0244 #non-interactive pin for the `--json` attach shape. Using the file's existing harness (its opts already plumb `json`, line 76, currently unused), add a test running `hyp attach --client claude --json` on a TTY against a proxy-capable descriptor and a key-less local gateway config, asserting: no prompt is issued (the askYesNo seam is never reached), stderr carries exactly the one pointer line `note: this install attaches claude by base URL; run 'hyp attach claude' in an interactive terminal to switch it to proxy mode`, no config write occurs (`proxy_mode` stays absent from the file on disk), and stdout remains the attach's valid JSON payload with nothing interleaved. Add a companion case proving `--json` combined with non-TTY still emits the pointer exactly once, not twice. Annotate with `// @ref LLP 0244#non-interactive [tests]: --json never prompts even on a TTY and emits exactly the pointer`. Gate: this file plus test/core/attach-enablement-state.test.js pass. No production code change is expected; if the assertion fails against `src/core/commands/clients.js` line 886's `parsed.json` gate, that is a bug to report, not a test to soften. -- id: T2 branch: task/proxy-mode-default-attach/T2 deps: [] complexity: 1 -- Corpus cross-links for the design of record: append `LLP 0246` to the `Related:` metadata line of llp/0242-fresh-installs-attach-base-url.issue.md, llp/0243-picker-composes-proxy-mode.decision.md, and llp/0244-attach-migrates-to-proxy-mode.decision.md (trivial editorial forward-refs, permitted on Accepted docs; touch only the metadata line, never a settled section body). Re-verify that the six anchors LLP 0246 and the code cite resolve: #composed-default and #user-key-wins in 0243, #attach-offers, #enable-write, #central-managed, and #non-interactive in 0244. Run the repo's ref-check skill over llp/ and the files carrying `@ref LLP 0243` / `@ref LLP 0244` annotations (src/core/cli/walkthrough.js, src/core/commands/clients.js, src/core/config/gateway_proxy_enable.js, hypaware-core/plugins-workspace/claude/src/index.js, the test files) and confirm zero broken references. Gate: ref-check clean; no test changes. +- id: T2 branch: task/proxy-mode-default-attach/T2 deps: [] complexity: 1 -- Corpus cross-links for the design of record: append `LLP 0251` to the `Related:` metadata line of llp/0242-fresh-installs-attach-base-url.issue.md, llp/0243-picker-composes-proxy-mode.decision.md, and llp/0244-attach-migrates-to-proxy-mode.decision.md (trivial editorial forward-refs, permitted on Accepted docs; touch only the metadata line, never a settled section body). Re-verify that the six anchors LLP 0251 and the code cite resolve: #composed-default and #user-key-wins in 0243, #attach-offers, #enable-write, #central-managed, and #non-interactive in 0244. Run the repo's ref-check skill over llp/ and the files carrying `@ref LLP 0243` / `@ref LLP 0244` annotations (src/core/cli/walkthrough.js, src/core/commands/clients.js, src/core/config/gateway_proxy_enable.js, hypaware-core/plugins-workspace/claude/src/index.js, the test files) and confirm zero broken references. Gate: ref-check clean; no test changes. diff --git a/llp/0246-proxy-mode-default-attach.design.md b/llp/0251-proxy-mode-default-attach.design.md similarity index 99% rename from llp/0246-proxy-mode-default-attach.design.md rename to llp/0251-proxy-mode-default-attach.design.md index 09cacafd..1c126217 100644 --- a/llp/0246-proxy-mode-default-attach.design.md +++ b/llp/0251-proxy-mode-default-attach.design.md @@ -1,4 +1,4 @@ -# LLP 0246: Proxy mode by default and the attach migration, technical design +# LLP 0251: Proxy mode by default and the attach migration, technical design **Type:** design **Status:** Active From c3964d4ba5c488950a41bb4c324bdeb009023567 Mon Sep 17 00:00:00 2001 From: test Date: Mon, 17 Aug 2026 22:05:04 +0000 Subject: [PATCH 6/6] Review: make the --json attach pins fail loudly instead of parking on an unanswerable prompt The new --json-on-a-TTY pin queued no stdin answer and leaned on askYesNo hanging as its guard. Removing the parsed.json term from the clients.js gate proved that inverts the guard: runAttach never settles, every assertion after it (including the one that names the bug) is skipped, withTempHome's cleanup never runs, and node reports cancelledByParent for the four tests behind it. Queue a 'y' like the sibling dry-run and attach-all cases, so a reached seam accepts the migration and trips the question, stderr, and config-unchanged assertions in place: one clean red test, no cascade. Also read the json flag off the attach args rather than the harness closure, the way a real adapter reads it, so the same pins fail if the command stops propagating --json into client.attach(). Verified both perturbations: gate removal now fails test 7 alone with testCodeFailure; dropping json from the attach args fails both new tests, where before this change both stayed green. --- test/core/attach-proxy-migration.test.js | 26 +++++++++++++++--------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/test/core/attach-proxy-migration.test.js b/test/core/attach-proxy-migration.test.js index e89a6395..ec7ff822 100644 --- a/test/core/attach-proxy-migration.test.js +++ b/test/core/attach-proxy-migration.test.js @@ -73,9 +73,9 @@ function writeGatewayConfig(home, opts) { * enable prompt never fires and the migration offer is the only question in * play. `answer` (when stdin is a TTY) is pre-buffered for it. * - * @param {{ home: string, answer?: string, tty?: boolean, json?: boolean }} opts + * @param {{ home: string, answer?: string, tty?: boolean }} opts */ -function makeCtx({ home, answer, tty = true, json = false }) { +function makeCtx({ home, answer, tty = true }) { const registered = ['claude', 'codex'] const gateway = { localEndpoint() { @@ -86,7 +86,7 @@ function makeCtx({ home, answer, tty = true, json = false }) { if (!registered.includes(name)) return undefined return { name, - /** @param {{ endpoint: string, dryRun?: boolean, stdout: { write(chunk: unknown): boolean } }} args */ + /** @param {{ endpoint: string, dryRun?: boolean, json?: boolean, stdout: { write(chunk: unknown): boolean } }} args */ async attach(args) { writeFileSync( path.join(home, `${name}-attached.json`), @@ -95,7 +95,11 @@ function makeCtx({ home, answer, tty = true, json = false }) { // Mirror a real adapter's --json contract: under json, stdout carries // exactly the one-line machine payload and nothing else, so the // --json pins below can assert it stays clean of any migration note. - if (json) { + // The flag is read off `args`, the way a real adapter reads it, so + // the pins also fail if the command stops propagating `--json` into + // `client.attach()` and every adapter starts printing prose into a + // machine-readable run. + if (args.json === true) { args.stdout.write( JSON.stringify({ status: 'ok', action: 'attach', client: name, dry_run: args.dryRun === true }) + '\n' ) @@ -242,11 +246,13 @@ test('--json on a TTY: no prompt, exactly one pointer note, no write, stdout sta await withTempHome(async (home) => { writeGatewayConfig(home) const before = readFileSync(localConfigPath(home), 'utf8') - // No `answer` is queued: if the askYesNo seam were somehow reached, the - // prompt would hang reading from an empty stdin, so this also guards - // against the seam being reached in a way a text assertion alone would - // miss. - const { ctx, stdout, stderr } = makeCtx({ home, tty: true, json: true }) + // A `y` is queued even though nothing may consume it: were the askYesNo + // seam ever reached, an unanswerable prompt would park on an empty stdin + // and the run would never settle, cancelling the rest of this file + // instead of naming the regression. With the answer buffered, a reached + // seam instead accepts the migration and trips the question, stderr, and + // config-unchanged assertions below, loudly and in place. + const { ctx, stdout, stderr } = makeCtx({ home, tty: true, answer: 'y' }) const code = await runAttach(['--client', 'claude', '--json'], ctx) assert.equal(code, 0, stderr.text()) // The askYesNo seam is never reached: its question never reaches stderr. @@ -275,7 +281,7 @@ test('--json combined with non-TTY still emits the pointer exactly once, not twi await withTempHome(async (home) => { writeGatewayConfig(home) const before = readFileSync(localConfigPath(home), 'utf8') - const { ctx, stdout, stderr } = makeCtx({ home, tty: false, json: true }) + const { ctx, stdout, stderr } = makeCtx({ home, tty: false, answer: 'y' }) const code = await runAttach(['--client', 'claude', '--json'], ctx) assert.equal(code, 0, stderr.text()) assert.ok(!stderr.text().includes(MIGRATION_QUESTION))