fix(sdk): give direct callers the CLI's extension option refusals and read-output admission

UN
6m ago
+1478-137

Summary

Direct callers of extension commands (PmClient.run, MCP pm_run, and the extension test harness) now get the refusals CLI argv gets. The codes match, and every refusal fires before the handler runs. The test harness also applies the native read-output admission. Two more defects in the same seam are fixed, and the complete-list recovery keeps a body request.

Closes #1726, closes #1727, closes #1728, closes #1729.

Direct-caller option contract (GH-1726, GH-1727)

ConditionBefore (direct callers)After (every caller)
Missing required flagexit 2, code: undefinedmissing_required_option with flag, field (handler key), recovery.missing, and a command example
Undeclared key on an extension-created commandreached the handlerunknown_option (flag)
Disabled flagno codeextension_option_disabled
Array for a single-value flag ([] or [true] for a strict boolean)reached the handlerextension_flag_value_invalid (flag)
Empty array for a required list flagacceptedmissing_required_option
  • CLI parity: the CLI path throws the same typed refusals. Before, it derived missing_required_option from the message text and printed a garbled hint plus pm create examples for extension commands.
  • Host-owned keys pass through: reserved host flags in camel, snake, or dashed form, the positive key of each --no-* switch, and the pm_run transport keys force, subcommand, inputResponses, and requestState.
  • Native paths: on a native command path that an extension extends (such as list), undeclared keys keep passing through. The native contract owns its aliases, negated flags, and settings-dependent runtime fields. This is the DirectExtensionOptionPolicy decision from Codex round 3.
  • Bare repeatable switch: on the CLI, a bare repeatable boolean switch reached the handler as [], because Commander hands the list accumulator undefined. Each bare occurrence is now true.
  • Native-field rewrites: PmClient.run applied the core-field array rewrites (tags to CSV; dep, comment, file, and test to arrays) to extension actions. Extension actions now keep the caller's values.
  • Bundled packages: pm-calendar's meet, event, and remind read inheritFocus without declaring it, so only direct callers could set it. They now declare --inherit-focus, and pm meet --no-inherit-focus works on the CLI for the first time. A census of the bundled packages found no other undeclared reads.
  • Out of scope: commands that declare no flags are unchanged. pm-4of199 waits on the fleet migration pm-kl1nio.

Harness read-output admission (GH-1729)

A command that declares read_output.row_keys must return those collections before any projection or output_format service runs. The native CLI has enforced this since 2026.11.13. The harness now enforces it too, on the same effective result native output admits:

  • after a registered command-result override;
  • skipping a result that suppresses host output;
  • with a structured output_format replacement admitted in place of the result;
  • with an explicit outputFormat from the options or the global output controls honoured.

The refusal now names the compatible pattern: keep returning the rows (an empty collection is valid) and let the formatter render the compact receipt. Oversized delivered output stays refused natively.

Complete-list recovery (GH-1728)

The recovery now repeats a body request: --include-body and listAllComplete({ includeBody: true }). It takes the request from an explicit PmCompleteListOptions argument or from the envelope's filters.include_body. The issue's secondary ask, dropping --no-truncate, is declined with evidence: certification requires filters.no_truncate, which only that flag echoes, and an executed retry without it is refused as page_incomplete. The hint mismatch is filed as pm-qsdlwd.

Tracker

  • Fixed: pm-gh1726, pm-gh1727 (newly filed for GH-1727), pm-gh1728, pm-gh1729. Each records a root cause, findings from Codex rounds 1–5, linked tests with recorded runs, contract_change, escape_class, and gate_evidence.
  • Follow-ups filed:
    • pm-g2zzus: mixed attached and bare repeatable switch order.
    • pm-qsdlwd: the --no-truncate migration hint makes a list uncertifiable.
  • Changelog regenerated with pm-changelog; the SDK contract changes render under "Changed (SDK contract)" with migration lines.

Review

Local Codex review (gpt-6.1-sol, xhigh, read-only) ran five rounds. Rounds 1–5 found harness/native parity edges:

  • native options, aliases, and negated keys on overridden core commands;
  • command-result overrides and structured output_format replacements;
  • inline declarations leaking their placeholders;
  • raw repeatable declarations;
  • output-encoding resolution and the service payload.

All of these are fixed, with specs in tests/unit/sdk/extension/harness-parity.spec.ts.

Verification

  • New and updated specs:
    • tests/integration/extensions/direct-caller-option-contract.integration.spec.ts (built CLI, PmClient.run, and harness, side by side)
    • tests/unit/sdk/extension/command-options.spec.ts
    • tests/unit/sdk/extension/harness-parity.spec.ts
    • tests/unit/sdk/query/complete-list.spec.ts
    • tests/integration/sdk-complete-list.integration.spec.ts (executes the emitted recovery)
    • tests/unit/packages/calendar-shortcuts.spec.ts
  • Generated artifacts and gates:
    • Regenerated: the error-code catalog, sdk/public-surface.json (acknowledged optional trailing arguments), and tests/fixtures/contracts/full.json.
    • Clean: ESLint, knip, the flag-lexicon and flag-invocation gates, and the defect-evidence gate.
  • No local full-suite verdict: this host is saturated by other repositories' runner jobs (load average 23–37 on 8 cores), so the full local suite timed out. The hosted coverage shards are the verdict.

Summary by Sourcery

Make extension command validation and output handling consistent across CLI and direct callers while preserving complete-list request semantics.

New Features:

  • Align direct extension command callers with CLI option validation and refusal codes across the SDK, MCP, and test harness.
  • Apply native read-output admission rules to extension test harness results.
  • Preserve body requests when generating complete-list recovery commands.

Bug Fixes:

  • Reject undeclared, disabled, missing, malformed, and incorrectly shaped extension options before handlers execute.
  • Correct repeatable boolean parsing and prevent native option rewrites from changing extension action values.
  • Declare the calendar shortcuts' inherit-focus option so it can be used consistently through the CLI and direct callers.

Enhancements:

  • Preserve native command option pass-through while enforcing stricter contracts for extension-created commands.
  • Improve complete-list certification recovery metadata and SDK guidance.

Documentation:

  • Document the unified direct-caller option refusal contract, read-output admission behavior, and body-preserving complete-list recovery.

Tests:

  • Add integration and unit coverage for CLI, SDK, MCP, harness, option validation, read-output admission, and complete-list recovery parity.

Chores:

  • Regenerate SDK contracts, error catalogs, fixtures, and refusal documentation.

Summary by cubic

Direct callers of extension commands (PmClient.run, MCP pm_run, and the extension test harness) now get the same option refusals as CLI argv, with matching codes fired before the handler runs. The harness also applies native read-output admission, and a refused complete list now repeats the caller's body request. Closes #1726, #1727, #1728, #1729.

Option refusals

  • Missing required flags, undeclared keys, disabled flags, and arrays for single-value flags are refused with missing_required_option, unknown_option, the new extension_option_disabled, and extension_flag_value_invalid.
  • A required list counts as missing when normalization drops all its entries (for example [""], [[]], or --ids ""), on both the CLI and direct paths.
  • Reserved host flags, the positive keys of --no-* switches, the pm_run transport keys (force, subcommand, inputResponses, requestState), and undeclared keys on native paths an extension extends keep passing through.
  • A negated spelling of a declared boolean with a value it cannot invert (for example { no-on: [] }) is now a value error naming the flag rather than unknown_option.
  • A bare repeatable boolean switch now records true per occurrence on the CLI, and loose-occurrence lists no longer drop Commander's recorded values for the same flag. Mixed attached/bare ordering is tracked as pm-g2zzus.
  • PmClient.run no longer applies the native array rewrites (tags, dep, etc.) to extension actions.
  • pm-calendar's meet, event, and remind now declare --inherit-focus.

Read-output admission and complete-list recovery

  • The harness now refuses a read-output result missing its declared row_keys, after command-result overrides, with structured output_format replacements admitted and dispatch metadata passed to those services.
  • Complete-list recovery now adds --include-body / includeBody: true when the refused request asked for bodies; custom transports pass the request's options as the second argument to certifyCompleteListResult.