Skip to content

SDK: lintExtensionBlueprint and assertExtensionPreflight pass a blueprint the loader hard-rejects, so the host-owned flag collision that quarantines an extension has no static check #784

Description

@unbraind

Summary

lintExtensionBlueprint() and assertExtensionPreflight() both return ok: true with zero findings for a blueprint declaring --json, which createExtensionTestHarness() / the loader then reject outright. The two APIs whose entire purpose is to vet a blueprint before activation are blind to the one defect class that disables a whole extension.

The data needed for the check is already public: @unbrained/pm-cli/sdk/contracts exports SUBCOMMAND_GLOBAL_FLAG_CONTRACTS, and the loader already produces an excellent message for the same condition.

Reproduction (2026.7.28, clean install)

import { lintExtensionBlueprint, assertExtensionPreflight } from "@unbrained/pm-cli/sdk/testing";

const blueprint = {
  name: "probe-ext", version: "1.0.0",
  activate(api) {
    api.registerCommand({
      command: "probe run", action: "run", description: "probe",
      flags: [{ long: "--json", description: "collides with host global", value_type: "boolean" }],
      handler: () => ({ ok: true }),
    });
  },
};

await lintExtensionBlueprint(blueprint);
// => { ok: true, findings: [] }

await assertExtensionPreflight(blueprint, { identity: { name: "probe-ext", version: "1.0.0" } });
// => { ok: true, findings: [] }

Activating the very same blueprint fails, correctly and with a great message:

createExtensionTestHarness could not activate the extension cleanly: project:probe-ext
(registerFlags flags[0] cannot shadow host-owned global flag "--json"; read it from
context.global instead)

Why this matters

This is the GH-719 / GH-764 defect class. It previously broke 9 of 18 extensions in our fleet at once — 22 shadowing flag declarations, each one quarantining its entire extension, all shipped green. 2026.7.28 making the loader hard-fail is a big improvement and we rely on it. But it only fires for authors whose tests actually activate the extension. An author who reaches for the purpose-built static checker — which is what lintExtensionBlueprint/assertExtensionPreflight advertise themselves as — gets an explicit all-clear on a blueprint that cannot run.

ExtensionBlueprintLintCode today is:

"capability_undeclared" | "capability_unused" | "duplicate_command" |
"command_override_conflict" | "empty_surface" | "reserved_item_field" |
"manifest_capabilities_absent"

Note reserved_item_field already exists — reserved item fields are linted, reserved flags are not, though the flag case has strictly worse consequences (a reserved item field is one field; a reserved flag is the whole extension).

Proposed fix

Add a host_owned_flag_collision code to ExtensionBlueprintLintCode at error severity, raised by lintExtensionBlueprint (and therefore surfaced through assertExtensionPreflight) whenever a declared flag's long value matches a SUBCOMMAND_GLOBAL_FLAG_CONTRACTS entry's flag or any of its aliases. Reusing the loader's existing wording — including the "read it from context.global instead" remediation — would make the static and runtime messages identical.

Secondary: key asymmetry makes the manual cross-check awkward

Writing this check by hand requires knowing that the same concept is spelled differently on each side:

  • extension-declared flags carry the token in FlagDefinition.long
  • the host contract carries it in SUBCOMMAND_GLOBAL_FLAG_CONTRACTS[].flag (+ .aliases)

So the join is flag.long === contract.flag, which reads like a bug at every call site. Not worth a breaking rename on its own, but if the lint rule above lands, no consumer needs to write the join at all.

Environment

  • @unbrained/pm-cli 2026.7.28 (fresh npm install, no local patches)
  • Node 24, Linux

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions