report-gap reports that Pluma fell short of what you needed: a missing
component, variant or prop, a layout or styling gap, an accessibility or docs
gap, or an API that got in the way. It sends the report to the handlers your
project lists in pluma.config.mjs,
so the gap reaches the people who can close it instead of turning up later as a
workaround.
Run it the moment Pluma falls short of what you need, before working around
the gap. Don't use it for a bug in a component that exists and should already
do what you need; file that as an issue against Pluma instead. Agents send the same
report through the report_gap tool of the Pluma MCP server (see
MCP twin).
| Flag | Description |
|---|---|
--category <c> | Required. One of missing_component, missing_variant, missing_prop, layout_gap, styling_gap, a11y_gap, api_friction, docs_gap, other. |
--intention <t> | Required. What you were trying to do, up to 2000 characters. |
--detail <t> | What you tried or built instead, up to 8000 characters. |
--component <id> | The kebab-case id of the Pluma component the gap is about, e.g. date-picker. |
--file <path> | The file you were working in. An absolute path picks the repository that holds it, and is recorded relative to its root. |
--source <s> | Who is reporting: human (the default), ai or automation. |
--confirm-public | Let handlers whose audience is public run. Without it they send nothing and are listed as consent_required. |
--json | Print the receipt (or, with --list-categories, the categories) as JSON. |
--list-categories | Print the categories, one per line. |
-h, --help | Show help. |
Each flag also takes the --name=value form, which a value that starts with
-- needs: --detail=--force broke the layout. -h and --help show help
only where a flag can go, so --intention -h records -h. Invalid input or an
unusable pluma.config.mjs exits with status 2 and delivers nothing; a failed
delivery, or a report no handler took, exits with status 1.
With a pluma.config.mjs that sends reports to
jsonlFile({ path: '.pluma/team-gaps.jsonl' }), the report is appended to that
file and the receipt lists one filed delivery:
The same report with --json prints the receipt as JSON:
Without a pluma.config.mjs, the report goes to the fallback file,
.pluma/gap-reports.jsonl. When the installed @customerio/pluma-components
names an issue tracker, the receipt says to file the gap there. Run where the
package isn't installed, as here, the delivery is filed:
--list-categories --json prints the categories as an array of strings.
Every handler receives the same report. This is the line jsonlFile appended
for the example in Report a gap:
component, detail and file appear only when given. target.version is
the @customerio/pluma-components installed in the node_modules of the
working directory (or of the directory holding an absolute file) or a
directory above it, and target.issuesUrl is the bugs URL in that
package's package.json. Either is null when it can't be found, as above,
where the example ran in a directory without the package installed.
report-gap prints a receipt: one delivery per handler, in order, and an
overall status. --json prints it as JSON:
A delivery's status is what its handler returned (filed, routed_only
or skipped), failed when the handler threw, returned something invalid or
ran past its 30 seconds, or consent_required for a public handler run
without --confirm-public. The overall status is:
partial when a delivery failed and another was filed or routed_only,
and failed when a delivery failed and none was;filed when any delivery was filed;routed_only when any was routed_only;consent_required when any was;failed: every handler skipped the report.With no handler configured, the report is appended to
.pluma/gap-reports.jsonl and the receipt has one
delivery with handler: "fallback". It is routed_only with the package's
issuesUrl, so you know where to file the gap, or filed with the file's URL
when the package names no issue tracker.
A handler is an object with an audience and a handle function:
audience: 'internal' handlers keep the report within your team and always
run. audience: 'public' handlers send it somewhere anyone can read, and run
only with --confirm-public (or confirmPublic: true from the MCP tool).handle gets its own copy of the report, and a signal that aborts after 30
seconds. A handler that throws or runs out of time is a failed delivery,
and the next handler still runs.filed when the handler stored the report, routed_only with a
url when it only knows where the report should go, and skipped to pass
on it.@customerio/pluma-cli/gap-report ships four handlers:
jsonlFile({ path }) appends each report as one JSON line to path,
resolved against the working directory, and returns filed.githubIssue({ repo, labels, audience }) files each report as an issue
in repo (owner/name) with the gh command, which must be installed and
signed in. The title is [gap] <category>: <component> (the start of the
intention when there is no component) and the labels are gap-report,
gap:<category>, source:<source> and any labels you add; they must
exist in the repository. When an open issue has the same labels and title,
the report becomes a comment on it instead. It returns filed with the
issue's URL.linearIssue({ teamId, labelIds, apiKeyEnv, audience, apiUrl }) files
each report as an issue in the Linear team teamId through Linear's GraphQL
API. The title and description match githubIssue's, and labelIds are the
ids of labels that exist in the team. When an open issue in the team has the
same title, the report becomes a comment on it instead. It returns filed
with the issue's URL. The API key is read from the LINEAR_API_KEY
environment variable, or the one apiKeyEnv names. audience defaults to
internal, and apiUrl to https://api.linear.app/graphql.slackMessage({ webhookUrlEnv, audience }) posts each report to a Slack
channel through an incoming webhook: a header with the category and
component, the intention and detail, and the file, source and package. The
webhook URL is read from the SLACK_GAP_REPORT_WEBHOOK_URL environment
variable, or the one webhookUrlEnv names. audience defaults to
internal. It returns filed with no URL.linearIssue and slackMessage read their secrets only from the environment,
when a report is delivered, so the API key and the webhook URL never go in
pluma.config.mjs. A missing variable, an error from the service, or a
response that isn't a success is a failed delivery. Both handlers accept a
fetch option that replaces the global fetch, for tests.
The package also exports the report types, GAP_CATEGORIES, and
reportGap(input, options), which builds a report and delivers it the way
report-gap does.
The report_gap tool of the Pluma MCP server
sends the same report through the same handlers in pluma.config.mjs, read
from the server's working directory, and returns the same receipt.
| CLI flag | report_gap parameter |
|---|---|
--category | category |
--intention | intention |
--detail | detail |
--component | component |
--file | file. Agents should pass an absolute path. |
--source | source. Defaults to ai instead of human. |
--confirm-public | confirmPublic: true. An agent passes it only after the user agreed to share the report publicly. |
--json | None: the tool always returns the receipt as JSON. |
--list-categories | None: the tool's category parameter lists the categories. |
Invalid input or an unusable pluma.config.mjs comes back as a tool error
instead of an exit status.
There is no pluma.config.mjs in the working directory, or it has no
gapReport key, so the report went to .pluma/gap-reports.jsonl at the
repository root. The delivery is routed_only when the installed
@customerio/pluma-components names an issue tracker: file the gap there by
hand. It is filed when the package names none, or isn't installed. To send
reports somewhere your team reads, add a gapReport handler to
pluma.config.mjs
and run report-gap from the directory that holds it.
githubIssue files issues with the gh command. spawn gh ENOENT means gh
isn't installed or isn't on your PATH; a message that starts with
gh failed: is gh's own error, such as not being signed in, no access to the
repository, or a label that doesn't exist there. The delivery is failed and
the command exits with status 1. Install gh and run gh auth login, check
that the account can open issues in the repository, and create the labels the
handler applies: gap-report, gap:<category>, source:<source> and any
labels you added.
A handler with audience: 'public' sends the report somewhere anyone can
read, so it runs only with --confirm-public. Without it the delivery is
consent_required and nothing is sent. Run the command again with
--confirm-public once you are sure the report can be public. Other handlers
in the list still run either way.
The command exits with status 2 and delivers nothing, to the handlers or the
fallback file. It prints every problem with the input at once: a missing or
unknown category, a missing or too long intention, a too long detail, a
component id that isn't kebab-case, or an unknown source. Run
pluma-cli report-gap --list-categories for the categories.
The same status follows a pluma.config.mjs that can't be used. The message
starts with the file's path and says what is wrong: it could not be loaded
(a syntax error, a missing import, or a handler such as githubIssue given a
repo that isn't owner/name), it needs a default export object, its
version is not one this release reads, or a gapReport entry needs a
handle(report, { signal }) function or an audience of "internal" or
"public".