happyskills 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,11 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.0.1] - 2026-06-01
11
+
12
+ ### Fixed
13
+
14
+ - Fix the post-rerank clarification step in `postlex`: the `clarify_triggered` telemetry beacon and the text-mode clarification prompt checked for a `clarify` action and read `suggested_questions` at the wrong depth, but the command emits `clarify_query` with the questions nested under `next_step.context` — so neither fired. The `--json` envelope was already correct; this restores the CLI's own telemetry and human-readable clarification output.
15
+
10
16
  ## [1.0.0] - 2026-05-29
11
17
 
12
18
  ### Added
13
19
 
14
- - **Universal response envelope on every `--json` command** (spec 260525-cli-default-json). All structured output now emits a canonical six-key shape — `{ ok, data, error, next_step, warnings, meta }` — with `ok` derived from error presence, `data` always an object, and the process exit code mirrored into `meta.exit_code`. Agents can determine success/failure and the recovery path without parsing prose.
20
+ - **Canonical six-key response envelope on every `--json` command** (spec 260525-cli-default-json). All structured output now emits a canonical six-key shape — `{ ok, data, error, next_step, warnings, meta }` — with `ok` derived from error presence, `data` always an object, and the process exit code mirrored into `meta.exit_code`. Agents can determine success/failure and the recovery path without parsing prose.
15
21
  - **`happyskills schema --json` command** — emits a machine-readable description of the entire CLI surface: every command, the closed `error_codes`, `next_step_actions`, and `next_step_kinds` enums. A generic agent can discover the full CLI contract in one call.
16
22
  - **Closed enums for the envelope** — `error.code` (state/auth/network/validation/etc.), `next_step.kind` (six kinds: recovery, clarification, decision, confirmation, continuation, routing), and `next_step.action` (~27 actions). Shipped as `cli/src/constants/{error_codes,next_step_actions}.js`, kept byte-identical with the API.
17
23
  - **Hand-rolled envelope validator** (`cli/src/schema/envelope_validator.js`) — enforces the closed schema, the `dependentRequired` clusters (code+message, kind+action+instructions), and the derived invariants. No new runtime dependency.
@@ -19,7 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
19
25
 
20
26
  ### Changed
21
27
 
22
- - **BREAKING: `--json` output format.** Every command's JSON output moved from the legacy `{ data }` / `{ error: { code, message, exit_code } }` shapes to the six-key envelope. `exit_code` now lives on `meta.exit_code`, not inside `error`. Consumers that parsed the old shapes must update — this is why the release is `1.0.0`.
28
+ - **BREAKING: `--json` output format.** Every command's JSON output moved from the legacy `{ data }` / `{ error: { code, message, exit_code } }` shapes to the canonical six-key response envelope. `exit_code` now lives on `meta.exit_code`, not inside `error`. Consumers that parsed the old shapes must update — this is why the release is `1.0.0`.
23
29
  - **CLI consumes the new API envelope** while remaining backward-compatible with the current (pre-envelope) API. The `search`, `feedback`, and device-login clients detect the envelope and translate; against the old API they fall through to the legacy path, so the new CLI works against both.
24
30
  - **`error.code` is a closed enum end-to-end** — unknown codes from a newer API coerce to `UNKNOWN_CODE` with the original preserved under `error.details.original_code`.
25
31
  - **Per-command `next_step` recoveries** now use the closed action enum across `install`, `reconcile`, `release`, `search`, `postlex`, `validate`, `delete`, and `feedback` (e.g. `VERSION_NOT_FOUND` → `pick_version`, `LOCAL_EDITS_PRESENT` → `confirm_discard_or_snapshot_first`, `DRIFT_DETECTED` → `reconcile_first`).
@@ -181,14 +187,14 @@ This release combines two streams of work: **spec 260523-02 (Skill Update Determ
181
187
  ## [0.47.0] - 2026-05-21
182
188
 
183
189
  ### Added
184
- - Add `--with-rerank` flag to `happyskills search`. When set, the CLI passes `with_rerank_digests=true` to `POST /repos:search` (via `dispatch_search()`) and wraps the `--json` response in the universal envelope shape `{ data, error, next_step }`. The `data` payload gains four new fields alongside the existing `results`: `rerank_digests`, `rerank_system_prompt`, `rerank_prompt_version`, `rerank_response_schema`, plus `formulated_query`. `next_step.action` is one of `rank_digests_inline` (digests came back — agent ranks them and pipes to `postlex`), `clarify` (semantic match_notice fired and clarification budget remains — agent uses its native question mechanism with a calibrated narrowing question), or `present_to_user` (budget exhausted / non-semantic mode — render results). Silently ignored on non-semantic dispatches (slug-shape, scoped, exact). Combining `--with-rerank --exact` exits 2 (`USAGE_ERROR`). Intended for use inside an agentic session — see the `happyskills-help@0.3.0` skill's `references/discovery-protocol.md` for the envelope contract.
190
+ - Add `--with-rerank` flag to `happyskills search`. When set, the CLI passes `with_rerank_digests=true` to `POST /repos:search` (via `dispatch_search()`) and wraps the `--json` response in the canonical six-key response envelope shape `{ data, error, next_step }`. The `data` payload gains four new fields alongside the existing `results`: `rerank_digests`, `rerank_system_prompt`, `rerank_prompt_version`, `rerank_response_schema`, plus `formulated_query`. `next_step.action` is one of `rank_digests_inline` (digests came back — agent ranks them and pipes to `postlex`), `clarify` (semantic match_notice fired and clarification budget remains — agent uses its native question mechanism with a calibrated narrowing question), or `present_to_user` (budget exhausted / non-semantic mode — render results). Silently ignored on non-semantic dispatches (slug-shape, scoped, exact). Combining `--with-rerank --exact` exits 2 (`USAGE_ERROR`). Intended for use inside an agentic session — see the `happyskills-help@0.3.0` skill's `references/discovery-protocol.md` for the envelope contract.
185
191
  - Add `--clarification-turns-used <N>` companion flag on `search`. Carries the clarification budget (0–2, default 0) across re-searches in the discovery protocol's clarification flow. The CLI clamps to the hard cap of 2 and emits `present_to_user` (instead of `clarify`) once the budget is spent.
186
192
  - Add new `happyskills postlex` subcommand — deterministic post-lex finalization for the discovery protocol. Takes the LLM's `ranking[]` JSON plus the original `data[]` array (from stdin via `--ranking -`, or from separate `--ranking <file>` + `--data <file>` paths), applies the canonical 30-line slug-overlap promotion algorithm, and emits a `next_step` envelope (`present_to_user` on success, `clarify` if the post-rerank top results are still weak and budget remains, `retry_rank` with an `input_template` when the ranking fails schema validation). Stateless — the agent carries all cross-call state via `--clarification-turns-used` and `next_step.context`. Human-readable output renders only the LLM-ranked subset (the unranked tail is not shown — the LLM chose not to rank those rows). Refuses to crash on out-of-range `candidate_id` values: invalid entries are dropped with a stderr warning; if every entry drops out, exit 1.
187
193
  - Add `cli/src/utils/slug_tokens.js` — STOP_TOKENS + `slug_tokens()` + `slug_token_set()` + `compute_lex_tier()`. Byte-identical mirror of `api/app/utils/slug_tokens.js`. The companion test (`slug_tokens.test.js`) cross-imports the API canonical version and asserts STOP_TOKENS set-equality in both directions — fails CI loud if either side drifts. This is the load-bearing invariant from spec 260521-01 v2 § 6: if the CLI's view of which candidate is "exact" disagrees with the API's slug-boost behavior, the post-lex stage promotes the wrong candidate (or none).
188
194
  - Add `cli/src/api/telemetry.js` — fire-and-forget client for `POST /telemetry/discovery`. 2-second `AbortController` timeout; all errors swallowed; never affects exit codes. Called from `search` (events `rerank_started` when emitting `rank_digests_inline`, `clarify_triggered` when emitting `clarify`, `clarify_completed` when re-running after a clarify cycle) and from `postlex` (event `rerank_completed` after the algorithm runs, plus `clarify_triggered` when emitting a post-rerank clarify). Server-side aggregation in CloudWatch Insights drives the two key signals: postlex fire rate (0.5%–5% target) and protocol completion rate (≥85% target — `count(rerank_completed)/count(rerank_started)`).
189
195
 
190
196
  ### Changed
191
- - `search --json` output is wrapped in the universal envelope `{ data, error, next_step }` when (and only when) `--with-rerank` is set. Plain `search --json` (without the flag) keeps its existing `{ data: { … } }` shape for backward compatibility — no consumer of the existing JSON contract sees a change.
197
+ - `search --json` output is wrapped in the canonical six-key response envelope `{ data, error, next_step }` when (and only when) `--with-rerank` is set. Plain `search --json` (without the flag) keeps its existing `{ data: { … } }` shape for backward compatibility — no consumer of the existing JSON contract sees a change.
192
198
  - Argument parser (`parse_args` in `cli/src/index.js`) now treats a literal `-` as a flag value instead of as the prefix of another flag. This is the standard Unix stdin sentinel and lets `happyskills postlex --ranking -` parse correctly. Conservative change: `next.startsWith('-') && next !== '-'` is the new gate, so any flag whose value would previously have been swallowed and replaced with `true` because the next arg happened to be `-` now correctly receives `'-'`. No existing command relied on the previous behavior.
193
199
 
194
200
  ## [0.46.1] - 2026-05-20
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "happyskills",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Package manager for AI agent skills",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Nicolas Dao <nic@cloudlesslabs.com> (https://cloudlesslabs.com)",
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
  // Post-Session-3 translator: near-identity. The public API now emits the
3
- // canonical six-key envelope natively (spec 260525-cli-default-json § 11),
3
+ // canonical six-key response envelope natively (spec 260525-cli-default-json § 11),
4
4
  // so this module only stamps the CLI's local `meta.cli_version` on top of
5
5
  // the response — meta.api_version is preserved verbatim. Forward-compat
6
6
  // for unknown error codes still routes through UNKNOWN_CODE per § 5.2.
@@ -3,7 +3,7 @@
3
3
  // Surfaces:
4
4
  // happyskills feedback <category> [body] [--subject "..."] [--attach a,b,c] [--json]
5
5
  //
6
- // --json mode emits the API's response wrapped in the universal CLI envelope:
6
+ // --json mode emits the API's response wrapped in the canonical six-key response envelope:
7
7
  // { data: { feedback, next_step, attachments? }, error: null }
8
8
  // The `next_step` envelope is what the `happyskills-help` skill reads to
9
9
  // route the post-creation conversation (offer attachment, etc.) — see spec
@@ -240,7 +240,7 @@ const run = (args) => catch_errors('Feedback command failed', async () => {
240
240
  }
241
241
 
242
242
  if (json_mode) {
243
- // Universal CLI envelope shape. The skill's envelope-reader (per spec
243
+ // Canonical six-key response envelope shape. The skill's envelope-reader (per spec
244
244
  // § 11) consumes `next_step` at the response root.
245
245
  const data = { feedback, attachments }
246
246
  print_json({ data, error: null, next_step })
@@ -4,7 +4,7 @@
4
4
  //
5
5
  // Spec 260521-01 v2 § 5.2 — this command is the deterministic safety net
6
6
  // for the rerank protocol. Frontier model emits ranking → postlex finalizes
7
- // → agent renders. The envelope makes the protocol legible to the agent
7
+ // → agent renders. The canonical six-key response envelope makes the protocol legible to the agent
8
8
  // without piling conditional logic into SKILL.md.
9
9
 
10
10
  const { error: { catch_errors, wrap_errors: e } } = require('puffy-core')
@@ -218,8 +218,30 @@ const determine_next_step = (final_ordering, query, clarification_turns_used) =>
218
218
  }
219
219
  }
220
220
 
221
+ // ─── next_step consumers (pure — exported for unit testing) ─────────────────
222
+ // run() reacts to the next_step that determine_next_step emitted: it fires a
223
+ // `clarify_triggered` telemetry beacon and renders a clarification notice in
224
+ // text mode. Those two decisions are factored out here so they can be
225
+ // unit-tested without spawning the process or hitting the telemetry endpoint.
226
+
227
+ // True when the emitted next_step is the post-rerank clarification step.
228
+ // The closed action enum value is `clarify_query` (next_step_actions.js) —
229
+ // matching the bare label "clarify" here silently disables the clarification
230
+ // beacon and notice, since determine_next_step never emits "clarify".
231
+ const is_clarify_next_step = (next_step) =>
232
+ !!next_step && next_step.action === 'clarify_query'
233
+
234
+ // The first suggested clarifying question to surface in text mode, or null.
235
+ // suggested_questions lives under next_step.context (spec § 4.5), not at the
236
+ // bare next_step root.
237
+ const first_clarify_question = (next_step) => {
238
+ const list = next_step && next_step.context && next_step.context.suggested_questions
239
+ const q = Array.isArray(list) && list[0]
240
+ return q && typeof q.question === 'string' ? q.question : null
241
+ }
242
+
221
243
  // Envelope used when the LLM's ranking is malformed and we need to ask it
222
- // to re-emit. Returns the canonical six-key envelope (spec § 4) so postlex
244
+ // to re-emit. Returns the canonical six-key response envelope (spec § 4) so postlex
223
245
  // can be unit-tested against the closed schema.
224
246
  const build_retry_envelope = (query, reason, clarification_turns_used, retry_count) => {
225
247
  const { build_envelope } = require('../ui/envelope')
@@ -450,7 +472,7 @@ const run = (args) => catch_errors('Postlex failed', async () => {
450
472
  promoted_from_rank,
451
473
  exact_match_count_in_window,
452
474
  })
453
- if (next_step.action === 'clarify') {
475
+ if (is_clarify_next_step(next_step)) {
454
476
  fire_discovery_telemetry({
455
477
  event: 'clarify_triggered',
456
478
  intent_id,
@@ -487,9 +509,9 @@ const run = (args) => catch_errors('Postlex failed', async () => {
487
509
  console.log(format_human_row(row))
488
510
  if (i < final_ordering.length - 1) console.log('')
489
511
  })
490
- if (next_step.action === 'clarify') {
512
+ if (is_clarify_next_step(next_step)) {
491
513
  console.log(`\n ${yellow('No top result is a strong match. Suggested clarification:')}`)
492
- console.log(` ${dim(next_step.suggested_questions[0].question)}`)
514
+ console.log(` ${dim(first_clarify_question(next_step))}`)
493
515
  } else if (next_step.action === 'present_to_user' && !final_ordering.slice(0, 3).every(r => STRONG_TIERS.has(r.match_quality))) {
494
516
  console.log(`\n ${yellow('No top result is a strong match (clarification budget spent).')}`)
495
517
  }
@@ -503,6 +525,8 @@ module.exports = {
503
525
  apply_postlex,
504
526
  build_final_ordering,
505
527
  determine_next_step,
528
+ is_clarify_next_step,
529
+ first_clarify_question,
506
530
  build_retry_envelope,
507
531
  parse_input,
508
532
  resolve_row_name,
@@ -14,6 +14,8 @@ const {
14
14
  apply_postlex,
15
15
  build_final_ordering,
16
16
  determine_next_step,
17
+ is_clarify_next_step,
18
+ first_clarify_question,
17
19
  build_retry_envelope,
18
20
  parse_input,
19
21
  resolve_row_name,
@@ -224,6 +226,52 @@ describe('determine_next_step', () => {
224
226
  })
225
227
  })
226
228
 
229
+ // ─── next_step consumer wiring (clarify_query drift regression) ────────────
230
+ // determine_next_step (the producer) emits next_step.action === "clarify_query"
231
+ // with suggested_questions nested under next_step.context (spec § 4.5). run()'s
232
+ // telemetry + text-render consumers must recognize that exact contract. The
233
+ // historical bug: the consumers checked for the bare label "clarify" and read
234
+ // next_step.suggested_questions at the wrong depth, so the clarify_triggered
235
+ // beacon never fired and the clarification notice never rendered — a SILENT
236
+ // failure (no error) at the one moment the harness is meant to engage.
237
+ describe('postlex clarify_query consumer wiring', () => {
238
+ const weak_ordering = [
239
+ { rank: 1, candidate_id: 1, match_quality: 'weak' },
240
+ { rank: 2, candidate_id: 2, match_quality: 'partial' },
241
+ { rank: 3, candidate_id: 3, match_quality: 'weak' },
242
+ ]
243
+
244
+ it('recognizes the producer\'s clarify_query next_step (telemetry + render gate)', () => {
245
+ const ns = determine_next_step(weak_ordering, 'deploy aws', 0)
246
+ assert.equal(ns.action, 'clarify_query', 'producer emits the canonical action')
247
+ assert.equal(
248
+ is_clarify_next_step(ns), true,
249
+ 'consumer must recognize action "clarify_query" so the clarify_triggered beacon fires and the notice renders',
250
+ )
251
+ })
252
+
253
+ it('reads the suggested question from next_step.context.suggested_questions', () => {
254
+ const ns = determine_next_step(weak_ordering, 'deploy aws', 0)
255
+ const q = first_clarify_question(ns)
256
+ assert.equal(typeof q, 'string', 'suggested question must be resolved from next_step.context')
257
+ assert.ok(q.length > 0, 'must surface the first suggested clarifying question for text-mode rendering')
258
+ })
259
+
260
+ it('does not treat the success path (present_to_user) as a clarify step', () => {
261
+ const ns = determine_next_step(
262
+ [
263
+ { rank: 1, candidate_id: 1, match_quality: 'strong' },
264
+ { rank: 2, candidate_id: 2, match_quality: 'good' },
265
+ { rank: 3, candidate_id: 3, match_quality: 'strong' },
266
+ ],
267
+ 'q', 0,
268
+ )
269
+ assert.equal(ns.action, 'present_to_user')
270
+ assert.equal(is_clarify_next_step(ns), false)
271
+ assert.equal(first_clarify_question(ns), null)
272
+ })
273
+ })
274
+
227
275
  // ─── build_retry_envelope ─────────────────────────────────────────────────
228
276
  // Envelope contract: six-key { ok, data, error, next_step, warnings, meta }
229
277
  // with data={}, error.code=RANKING_SCHEMA_MISMATCH (closed enum), kind=recovery,
@@ -305,7 +305,7 @@ const run_smart_search = (args, query, options) => catch_errors('Smart search fa
305
305
  data.rerank_prompt_version = response?.rerank_prompt_version || null
306
306
  data.rerank_response_schema = response?.rerank_response_schema || null
307
307
  }
308
- // Wrap in the universal envelope shape only when --with-rerank is set.
308
+ // Wrap in the canonical six-key response envelope shape only when --with-rerank is set.
309
309
  // Plain search keeps its existing { data: ... } shape for backward compat.
310
310
  print_json(with_rerank ? { data, error: null, next_step } : { data })
311
311
  return
@@ -91,7 +91,7 @@ describe('build_search_next_step — envelope shape', () => {
91
91
  const r = build_search_next_step(make_response({
92
92
  match_notice: 'No strong or good matches.',
93
93
  }), 'vague query', { with_rerank: true, clarification_turns_used: 0 })
94
- assert_envelope_next_step(r, NEXT_STEP_ACTIONS.CLARIFY_QUERY, 'clarify')
94
+ assert_envelope_next_step(r, NEXT_STEP_ACTIONS.CLARIFY_QUERY, 'clarify_query')
95
95
  // suggested_questions and max_turns_remaining now live INSIDE context.
96
96
  assert.strictEqual(r.context.max_turns_remaining, 2)
97
97
  assert.ok(Array.isArray(r.context.suggested_questions))
@@ -1,5 +1,5 @@
1
1
  'use strict'
2
- // Closed enum of error codes emitted by the CLI envelope. Mirrors spec
2
+ // Closed enum of error codes emitted by the canonical six-key response envelope. Mirrors spec
3
3
  // 260525-cli-default-json § 5. Skeleton-only — Session 2 wires it into
4
4
  // emit_envelope / exit_with_error. Until then, only the test suite consumes
5
5
  // it (via envelope_validator).
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
  /**
3
3
  * End-to-end integration tests: CLI ↔ stubbed API speaking the new
4
- * canonical six-key envelope (spec 260525-cli-default-json § 4).
4
+ * canonical six-key response envelope (spec 260525-cli-default-json § 4).
5
5
  *
6
6
  * We spin up a tiny `http.createServer` per test that emits the EXACT
7
7
  * envelope the production API now returns (post-Session 3), point the
@@ -33,7 +33,7 @@ const NODE = process.execPath
33
33
 
34
34
  // Minimal stub-server harness. Each test installs a route table that maps
35
35
  // `${METHOD} ${PATH}` → handler({ url, body }) returning { status, body }.
36
- // The body is always JSON-encoded with the new six-key envelope shape.
36
+ // The body is always JSON-encoded with the new canonical six-key response envelope shape.
37
37
  //
38
38
  // IMPORTANT: every response carries `Connection: close`. Without this,
39
39
  // undici's connection pool keeps the socket warm and prevents the CLI
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
  /**
3
3
  * Integration tests for `happyskills reconcile` — § 8.4 + envelope refactor
4
- * (spec 260525-cli-default-json). Asserts on the SIX-KEY envelope shape:
4
+ * (spec 260525-cli-default-json). Asserts on the canonical six-key response envelope shape:
5
5
  * top-level `ok / data / error / next_step / warnings / meta`, closed enums,
6
6
  * dependentRequired clusters. Every --json invocation is validated via the
7
7
  * shared parse_envelope helper so this file doubles as a conformance test.
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
  /**
3
3
  * Integration tests for `happyskills release` — § 8.2 + envelope refactor
4
- * (spec 260525-cli-default-json). Asserts on the six-key envelope shape and
4
+ * (spec 260525-cli-default-json). Asserts on the canonical six-key response envelope shape and
5
5
  * the closed `next_step.action` enum.
6
6
  *
7
7
  * Focus: the failure-mode envelopes (drift, missing_version, missing_changelog
@@ -6,7 +6,7 @@
6
6
  * command, every error code, and every next_step.action in one call.
7
7
  *
8
8
  * Coverage:
9
- * - Envelope conforms to the closed schema (validated via parse_envelope).
9
+ * - The canonical six-key response envelope conforms to the closed schema (validated via parse_envelope).
10
10
  * - data.envelope_schema_version + data.envelope_schema_uri present.
11
11
  * - data.commands carries one entry per known CLI command, each with
12
12
  * name, audience, input, output, errors[].
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
  // The single chokepoint for CLI JSON output. Spec 260525-cli-default-json
3
3
  // § 4 + § 8. Every command's --json path goes through emit_envelope; the
4
- // helper builds the canonical six-key envelope, derives `ok`, mirrors the
4
+ // helper builds the canonical six-key response envelope, derives `ok`, mirrors the
5
5
  // exit code into `meta`, validates against the closed schema (dev
6
6
  // assertion only — never throws in prod), serialises with 2-space indent,
7
7
  // writes to stdout, and (when called via emit_and_exit) calls process.exit.
package/src/ui/output.js CHANGED
@@ -110,14 +110,14 @@ const summarize_warnings = (warnings, skill_name) => {
110
110
 
111
111
  // print_json now routes through the central envelope chokepoint when given
112
112
  // an envelope-shaped input ({ data }, { error }, { next_step }, or a full
113
- // six-key envelope). This retrofits every legacy `print_json({ data: ... })`
113
+ // canonical six-key response envelope). This retrofits every legacy `print_json({ data: ... })`
114
114
  // call site to emit the new schema without changing the call. Raw inputs
115
115
  // (e.g. a bare array) fall through to the legacy JSON.stringify path so
116
116
  // scripts/validators that print free-form JSON still work.
117
117
  //
118
118
  // Audit follow-up #6 — dev-mode strict assertion. When
119
119
  // HAPPYSKILLS_ENVELOPE_STRICT=1 (or NODE_ENV=test), any top-level key
120
- // outside the six-key envelope is written to stderr as a warning. This
120
+ // outside the canonical six-key response envelope is written to stderr as a warning. This
121
121
  // catches the `uninstall.js:79` / `install.js:244` class bug — callers
122
122
  // passing a sibling field (`errors:` plural, `attachments:`, etc.) that
123
123
  // the retrofit would silently drop. Production stays quiet.