fix(sdk): give direct callers the CLI's extension option refusals and read-output admission
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)
| Condition | Before (direct callers) | After (every caller) |
|---|---|---|
| Missing required flag | exit 2, code: undefined | missing_required_option with flag, field (handler key), recovery.missing, and a command example |
| Undeclared key on an extension-created command | reached the handler | unknown_option (flag) |
| Disabled flag | no code | extension_option_disabled |
Array for a single-value flag ([] or [true] for a strict boolean) | reached the handler | extension_flag_value_invalid (flag) |
| Empty array for a required list flag | accepted | missing_required_option |
- CLI parity: the CLI path throws the same typed refusals. Before, it derived
missing_required_optionfrom the message text and printed a garbled hint pluspm createexamples 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 thepm_runtransport keysforce,subcommand,inputResponses, andrequestState. - 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 theDirectExtensionOptionPolicydecision from Codex round 3. - Bare repeatable switch: on the CLI, a bare repeatable boolean switch reached the handler as
[], because Commander hands the list accumulatorundefined. Each bare occurrence is nowtrue. - Native-field rewrites:
PmClient.runapplied the core-field array rewrites (tagsto CSV;dep,comment,file, andtestto arrays) to extension actions. Extension actions now keep the caller's values. - Bundled packages:
pm-calendar'smeet,event, andremindreadinheritFocuswithout declaring it, so only direct callers could set it. They now declare--inherit-focus, andpm meet --no-inherit-focusworks 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_formatreplacement admitted in place of the result; - with an explicit
outputFormatfrom 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, andgate_evidence. - Follow-ups filed:
- pm-g2zzus: mixed attached and bare repeatable switch order.
- pm-qsdlwd: the
--no-truncatemigration 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_formatreplacements; - inline declarations leaking their placeholders;
- raw
repeatabledeclarations; - 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.tstests/unit/sdk/extension/harness-parity.spec.tstests/unit/sdk/query/complete-list.spec.tstests/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), andtests/fixtures/contracts/full.json. - Clean: ESLint, knip, the flag-lexicon and flag-invocation gates, and the defect-evidence gate.
- Regenerated: the error-code catalog,
- 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 newextension_option_disabled, andextension_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, thepm_runtransport 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 thanunknown_option. - A bare repeatable boolean switch now records
trueper 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.runno longer applies the native array rewrites (tags,dep, etc.) to extension actions.pm-calendar'smeet,event, andremindnow 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 structuredoutput_formatreplacements admitted and dispatch metadata passed to those services. - Complete-list recovery now adds
--include-body/includeBody: truewhen the refused request asked for bodies; custom transports pass the request's options as the second argument tocertifyCompleteListResult.