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
Summary
lintExtensionBlueprint()andassertExtensionPreflight()both returnok: truewith zero findings for a blueprint declaring--json, whichcreateExtensionTestHarness()/ 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/contractsexportsSUBCOMMAND_GLOBAL_FLAG_CONTRACTS, and the loader already produces an excellent message for the same condition.Reproduction (2026.7.28, clean install)
Activating the very same blueprint fails, correctly and with a great message:
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/assertExtensionPreflightadvertise themselves as — gets an explicit all-clear on a blueprint that cannot run.ExtensionBlueprintLintCodetoday is:Note
reserved_item_fieldalready 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_collisioncode toExtensionBlueprintLintCodeaterrorseverity, raised bylintExtensionBlueprint(and therefore surfaced throughassertExtensionPreflight) whenever a declared flag'slongvalue matches aSUBCOMMAND_GLOBAL_FLAG_CONTRACTSentry'sflagor any of itsaliases. Reusing the loader's existing wording — including the "read it fromcontext.globalinstead" 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:
FlagDefinition.longSUBCOMMAND_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-cli2026.7.28 (freshnpm install, no local patches)