You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
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
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.
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.
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)
Check the box below or use the @coderabbitai plan command to generate an implementation plan and prompts that you can use with your favorite coding assistant.
Create Plan
🧪 Issue enrichment is currently in open beta.
You can configure auto-planning by selecting labels in the issue_enrichment configuration.
To disable automatic issue enrichment, add the following to your .coderabbit.yaml:
issue_enrichment:
auto_enrich:
enabled: false
💬 Have feedback or questions? Drop into our discord!
Correcting my own reproduction above, and adding a second finding in the same validator.
Corrected reproduction
The snippet I first posted was itself invalid — the command definition was missing name, and registerCommand with flags requires the schema capability. It therefore failed activation for unrelated reasons. The conclusion is unchanged and still reproduces, but here is a repro that actually runs:
--fine lint.ok=true activation=ACCEPTED
--json lint.ok=true activation=REJECTED <-- the reported gap
verbose lint.ok=true activation=ACCEPTED <-- second finding, below
-v lint.ok=true activation=ACCEPTED <-- second finding, below
Row 2 is the original report: lint says the blueprint is clean, the loader rejects it with registerFlags flags[0] cannot shadow host-owned global flag "--json".
Second finding: flag token form is never validated
Rows 3 and 4 are a separate gap that surfaced while building the corrected repro. A flag declared as verbose (no leading dashes) or -v (single dash, i.e. a short flag in the long field) is accepted by both the linter and the loader, and the token is stored verbatim:
Scoping this claim honestly: I verified that the token is stored as-written and that neither validator objects. I did not verify end-to-end that the real CLI parser fails to bind verbose/-v — that needs a fully installed extension against real argv, which I have not run. So I am reporting this as "the declared form is never validated", not as "the flag is definitely unreachable". If the parser does normalize these downstream then this is only a lint nit; if it does not, the flag is silently dead in exactly the way GH-719 flags were.
Either way it seems worth deciding deliberately, since both cases are cheap to catch in the same place as the collision rule — a long that does not match /^--[a-z0-9][a-z0-9-]*$/ is almost certainly an authoring typo.
Tracked canonically in pm-huolbk. The item links the closed author-time lint and GH-719/GH-764 runtime-collision lineage, includes both the host-owned collision and malformed long token findings, and remains open/unclaimed.
Activity
coderabbitai commentedon Jul 28, 2026
🔗 Related PRs
#347 - feat(sdk): author-time blueprint introspection & preflight (describeExtensionBlueprint + lintExtensionBlueprint + assertExtensionBlueprint) [merged]
#351 - feat(sdk): unified author-time preflight capstone — preflightExtension + assertExtensionPreflight [merged]
#479 - feat(package): isolate project doctor diagnostics [merged]
#561 - SDK: unify extension input and output host contracts [merged]
#657 - SDK extension authoring, trust boundaries, and scoped rendering [merged]
#734 - SDK host contracts, live agent context, and portable extension delivery [merged]
📝 Issue Planner
Check the box below or use the
@coderabbitai plancommand to generate an implementation plan and prompts that you can use with your favorite coding assistant.🧪 Issue enrichment is currently in open beta.
You can configure auto-planning by selecting labels in the issue_enrichment configuration.
To disable automatic issue enrichment, add the following to your
.coderabbit.yaml:💬 Have feedback or questions? Drop into our discord!
unbraind commentedon Jul 28, 2026
Correcting my own reproduction above, and adding a second finding in the same validator.
Corrected reproduction
The snippet I first posted was itself invalid — the command definition was missing
name, andregisterCommandwithflagsrequires theschemacapability. It therefore failed activation for unrelated reasons. The conclusion is unchanged and still reproduces, but here is a repro that actually runs:Output on 2026.7.28:
Row 2 is the original report: lint says the blueprint is clean, the loader rejects it with
registerFlags flags[0] cannot shadow host-owned global flag "--json".Second finding: flag token form is never validated
Rows 3 and 4 are a separate gap that surfaced while building the corrected repro. A flag declared as
verbose(no leading dashes) or-v(single dash, i.e. a short flag in thelongfield) is accepted by both the linter and the loader, and the token is stored verbatim:No normalization, no warning, no error.
Scoping this claim honestly: I verified that the token is stored as-written and that neither validator objects. I did not verify end-to-end that the real CLI parser fails to bind
verbose/-v— that needs a fully installed extension against real argv, which I have not run. So I am reporting this as "the declared form is never validated", not as "the flag is definitely unreachable". If the parser does normalize these downstream then this is only a lint nit; if it does not, the flag is silently dead in exactly the way GH-719 flags were.Either way it seems worth deciding deliberately, since both cases are cheap to catch in the same place as the collision rule — a
longthat does not match/^--[a-z0-9][a-z0-9-]*$/is almost certainly an authoring typo.unbraind commentedon Jul 28, 2026
Tracked canonically in pm-huolbk. The item links the closed author-time lint and GH-719/GH-764 runtime-collision lineage, includes both the host-owned collision and malformed
longtoken findings, and remains open/unclaimed.unbraind commentedon Jul 28, 2026
Implemented in #798 and tracked in pm-huolbk.