The first draft read #1834's "draft, not interrogate" literally and turned it into a contract: a scenario requiring the skill to ask only what it cannot infer and to show one complete draft rather than a sequence of questions. That forbids the thing most worth doing. A vague issue filed instantly is not cheaper than a good one filed a minute later — it is the same work, moved to a maintainer and made harder by the missing context. Split the contract by what the skill is asking about: - Facts are the skill's job to find. The version, the platform, the failing command, the agent and model are all observable, and asking for any of them is the interrogation users actually resent. This half of "draft, not interrogate" survives as a prohibition. - Judgements are the user's. What the report is really asking for, who it affects, what it excludes, whether the reported problem is the problem or a symptom. Each question carries a recommended answer so agreement costs a word, and questions are ordered by dependency so none is asked before the decision it rests on. Refinement ends by exhaustion rather than at a fixed count, and asking nothing at all is a valid outcome. The spec fixes the ordering rule and the recommendation, not the cadence, so the question technique can improve without a spec change. Also records why refinement cannot live in the CLI (a fixed question list asks a crash and a papercut the same five things), and why the skill has to stay one file: generateSkillContent returns a single string and every adapter writes one SKILL.md, so the progressive disclosure current skill-authoring practice assumes is not available without its own proposal. Leanness is therefore a constraint — explain the reasoning instead of stacking capitalised MUSTs, and phrase instructions positively. Part of #1834 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
16 KiB
Context
See proposal.md — Why. Three facts about the existing code shape every decision below.
- Workflow ids are a wide seam, and part of it is compile-enforced.
WORKFLOW_TO_SKILL_DIRinsrc/core/profile-sync-drift.ts:28is typedRecord<WorkflowId, string>, so adding an id toALL_WORKFLOWSwithout adding it there failstsc. Four other maps carry the same information without that protection: the duplicateWORKFLOW_TO_SKILL_DIRinsrc/core/init.ts:111(typedRecord<string, string>),COMMAND_TO_SKILL_NAMEinsrc/utils/command-references.ts:53,OPENSPEC_SKILL_NAMESinsrc/core/config.ts:3, andCOMMAND_IDSinsrc/core/shared/tool-detection.ts:41. Only the first is protected; the other four drift silently, because the tests around them assert fixed counts rather than derive fromALL_WORKFLOWS. feedbackis the only workflow module with no command template. Every othersrc/core/templates/workflows/*.tsexports agetOpsx*CommandTemplatebeside its skill template.getCommandTemplates()andCOMMAND_IDSassume the pair, andprofile-sync-driftreads a missing command file underdelivery: 'both'as drift to repair on every run.- The core profile is the default, and the only profile most users ever have.
getProfileWorkflows(src/core/profiles.ts:45) returnsCORE_WORKFLOWSunconditionally for any non-customprofile and ignorescustomWorkflowsentirely forcore;DEFAULT_CONFIG.profileis'core'(src/core/global-config.ts:45). An id added toALL_WORKFLOWSalone reaches a user only if they openopenspec config profileand tick it, which also flips them tocustom.
Goals / Non-Goals
Goals
- A user who says "file that as an issue" gets a well-scoped issue, without reading anything first.
- The issue arrives needing less work from a maintainer than it would have. That is the point: refinement at filing time is labour not spent in triage later.
- A skill-filed issue and a form-filed issue carry the same fields, so triage reads one shape.
openspec feedbackwith no flags behaves exactly as it does today.
Non-Goals
- CI enforcement that every PR links an issue. #1834 rules it out, and a gate punishes the people the easy path has not reached yet.
- Sending anything anywhere without an explicit confirmation. Feedback submission
stays a deliberate act, independent of telemetry
(
cli-feedback— "Feedback always works"). - Reading the issue forms at runtime. The CLI never fetches or parses
.github/; see "Coupling to the forms" below. - Refinement in the CLI. Deciding which question matters next, given what the
user just said, is the whole value and it needs a model. A fixed question list
in
feedback.tswould ask a crash and a papercut the same five things and could not tell a vague answer from a usable one. The CLI keeps the deterministic half — validating--type, assembling the body, matching the form's fields and labels, building the fallback URL — and the skill keeps the judgement.
Decisions
Keep the workflow id feedback
#1834 leaves this open: keep feedback, or rename to report?
Keep feedback. It is already the CLI verb (openspec feedback) and already the
capability name (openspec/specs/cli-feedback/). Renaming would split the skill from the command it tells the agent
to run, for a word that is no clearer at the point of use — a user asking to
"report a bug" and a user asking to "send feedback" both want the same skill, and
the skill's description is what an agent matches on, not its id.
report would also be the first workflow id that names a document rather than an
act, against propose / apply / archive / verify.
Make it a core workflow, not an optional one
This is the one decision here that changes the default install for every user, and it is the decision most worth arguing with.
The case for optional: the six core workflows are the steps of one loop — propose,
explore, apply, update, sync, archive. feedback is not a step in that loop. Every
core workflow costs a file in every configured tool's skill directory for every
user, forever, and a skill about the tool itself is a weaker claim on that space
than a skill about the user's work.
The case for core, which we take: the problem #1834 describes is not that the
feedback skill is hard to find, it is that nobody has it. Shipping it as opt-in
reproduces the current outcome at a smaller scale — the users who would tick a box
in openspec config profile are the users already able to write a good issue by
hand. The whole value is in reaching the user who has not read anything, and only
the default install reaches them.
The cost is bounded and reversible: one skill, one command file, no change to any
other workflow, and moving it out of CORE_WORKFLOWS later is a one-line change
with the same update path in reverse.
Consequence to handle rather than assume: existing projects on a custom profile
must not silently gain a workflow they did not choose.
displayMissingCoreWorkflowsNote (src/core/update.ts:689, called at :656)
already handles this: it returns early unless the profile is custom, diffs
CORE_WORKFLOWS against the user's workflows, points at openspec config profile, and mutates nothing. This change relies on that path rather than adding
one.
--type defaults to feedback, and each type carries the form's labels
--type |
Title | Body sections | Labels |
|---|---|---|---|
feedback (default) |
Feedback: <msg> |
Summary, Details | feedback |
bug |
<msg> |
What happened, Expected, Repro, Version, Agent, Environment | bug, needs-triage |
feature |
<msg> |
Problem, Who it affects, What you tried, Proposal | enhancement, needs-triage |
Four things follow from this table.
The default is feedback because the bare command must not change behavior for
anyone typing it today. --type bug and --type feature drop the Feedback:
prefix, because a title reading Feedback: archive drops the main spec misfiles
a bug report.
The default keeps asking for the feedback label, and the fix for that label not
existing is to create it, not to stop asking. The alternative — dropping the label
request for the default type — buys one saved round trip and costs the ability to
find general feedback in the issue list at all, which is the thing the label is
for. bug and enhancement already exist. The missing-label retry stays for all
three, because a label that exists today can be renamed tomorrow and a renamed
label must not cost a user their report.
The labels per type are the forms' labels, needs-triage included. A report
filed by the CLI and a report filed through the form have to land in the same
triage queue, or the CLI path quietly skips it.
The fields per type are the issue forms' fields, in the forms' order. That is the point: the same report, whoever files it and however.
The manual fallback targets the form, and degrades to today's URL
generateManualSubmissionUrl() gains template=bug_report.yml (or
feature_request.yml) and one query parameter per form field id — GitHub's own
prefill mechanism, which needs no API and no auth.
Two failure modes are handled rather than hoped away.
Length. A prefilled URL carries the whole body twice-encoded, and a long
report can exceed what GitHub and some browsers accept. The builder caps the
encoded URL; over the cap it returns the blank-issue URL the command builds today,
with the body intact. A user who lands on a blank form with their text still in it
has lost formatting, not their report. --type feedback uses that blank-issue URL
unconditionally, since it has no form to target.
Coupling to the forms. The field ids (what_happened, expected, repro,
version, agent, environment; problem, who, tried, proposal) become an
interface the CLI depends on and .github/ owns. The CLI does not read .github/
— it cannot, since it runs in the user's project. It hard-codes the ids, and a
test asserts that the ids it hard-codes are exactly the ids present in
.github/ISSUE_TEMPLATE/*.yml, so renaming a field in a form fails CI in this
repository rather than silently emptying a field for users. If an id ever does go
stale in a released version, GitHub ignores unknown prefill parameters: the user
gets the right form with one field blank, not an error.
What the skill asks, and what it must never ask
The current template's flow is "review the conversation, draft, show it." #1834 sharpens that to "draft, not interrogate," and taken literally that forbids the thing most worth doing: helping the user work out what they are actually asking for. A vague issue filed instantly is not cheaper than a good one filed a minute later — it is the same work, moved to a maintainer and made harder by the missing context. So the skill refines first, and the contract is about which things it may put to the user.
The split is facts versus judgements.
Facts are the skill's job. The OpenSpec version, the platform, the failing command and its output, the agent and model, what the user was doing — all observable. Asking for any of them is the interrogation failure mode, and it is the one users actually resent. This is the half of #1834's "draft, not interrogate" that survives intact, and it is stated as a prohibition in the spec because it is the one place a hard line is right.
Judgements are the user's. What the report is really asking for, who it affects, what it deliberately excludes, whether the reported problem is the problem or a symptom of one. These cannot be inferred, and they are exactly the questions whose absence costs a maintainer a round trip later. Scope is the highest-value one: what should this not do settles more downstream argument than any reproduction detail.
Two mechanics are worth borrowing rather than inventing, from Matt Pocock's
grilling skill, which does this well:
- Each question carries a recommended answer. Agreement then costs a word, and
a user who disagrees is arguing with a concrete proposal instead of composing
one. This is also how OpenSpec's own
explorealready works — its sharedPLANNING_GUIDANCEsays to "state your preferred option and why it fits." - Dependency ordering. Never ask a question whose answer depends on one still
open; settle the blocking decision, then the details it unlocks.
explorealready says "Follow dependencies"; grilling makes it the whole selection rule.
Grilling asks the entire unblocked frontier in a numbered round; explore asks
one focused question at a time. The spec deliberately picks neither. Cadence is
the kind of thing that should improve without a spec change, and the right answer
probably differs between a bug report and a feature idea. What the spec fixes is
the ordering rule and the recommendation, which are the parts that make the
conversation short.
Termination is by exhaustion, not by count. Grilling refuses a question cap on purpose, and it is right to: some reports need one question and some need ten. What the spec requires instead is that the skill stop when nothing material is unsettled — and, just as importantly, that it be able to ask nothing at all. A skill that manufactures three questions for a typo report teaches users to stop invoking it, which costs more than any single badly-scoped issue.
One file, no reference layer
Current skill-authoring practice — Anthropic's skill-creator, and Pocock's
skills, whose SKILL.md files run 7 to 140 lines — is progressive disclosure: a
short SKILL.md that points at sibling reference files loaded only when needed.
OpenSpec cannot do that today. generateSkillContent
(src/core/shared/skill-generation.ts:132) returns a single string, and every
adapter writes exactly one SKILL.md per skill directory. There is no mechanism
for a sibling file — adding one would touch the generator, every adapter's file
layout, tool-detection.ts's existence checks, the skills/ mirror, and the
parity hashes. That is its own proposal, and this change should not smuggle it in.
So the whole skill has to fit one file, which makes leanness a constraint rather than a preference. Two consequences for how it is written, both drawn from the same current practice:
- Explain why instead of stacking capitalised MUSTs. The existing template has
seven in a row under "Guardrails."
skill-creatorcalls that a yellow flag and asks for the reasoning instead, so the model can generalise to the case nobody wrote down. The genuine invariants — show the draft, get confirmation, anonymise — stay imperative; the rest becomes explanation. - Phrase instructions positively. Pocock's
writing-for-agentsputs it well: steering by prohibition drags the forbidden behaviour into context and makes it more available. "Fill the version in yourself" beats "DO NOT ask for the version."
The skill's own name has to change, even though the workflow id does not
The workflow id and the skill's frontmatter name are different things, and
feedback is currently wrong in the second. dirName is a free literal per entry
in getSkillTemplates() (src/core/shared/skill-generation.ts:61), but the
SKILL.md frontmatter is written from template.name
(src/core/shared/skill-generation.ts:142), and every registered template sets
name equal to its dirName — openspec-verify-change, openspec-onboard, and so
on. src/core/templates/workflows/feedback.ts:11 sets name: 'feedback', written
when nothing deployed it.
Registering it as-is would commit skills/openspec-feedback/SKILL.md whose
frontmatter says feedback — the only skill where the two disagree, and a
disagreement with both OPENSPEC_SKILL_NAMES and the /openspec-feedback
reference in COMMAND_TO_SKILL_NAME. The template's name becomes
openspec-feedback; the workflow id stays feedback, exactly as verify maps to
openspec-verify-change.
The command template must take $ARGUMENTS
test/core/command-generation/adapters.test.ts:158 asserts that onboard is the
only command with no $ARGUMENTS placeholder, and the file describes that
assertion as a tripwire for exactly this situation. /opsx-feedback <what went wrong> should accept the message anyway — an agent invoking it already has the
sentence.
Risks / Trade-offs
- A seventh core skill for every user. Accepted above, and cheap to reverse.
- Five parallel id maps, three unprotected. This change adds a sixth entry to
each. It does not consolidate them — that is a refactor with its own blast
radius and belongs in its own proposal — but the tasks below touch all of them
in one pass. Be clear about what actually catches an omission: only
tsc, via theRecord<WorkflowId, string>atprofile-sync-drift.ts:28. The other guards are fixed-count assertions such astool-detection.test.ts:33, which fail when you add an entry and stay silent when you skip one, andcommand-references.test.ts:92, whose list is hand-written and does not derive fromALL_WORKFLOWS— it omitspropose, which is in the map. OmittingfeedbackfromCOMMAND_TO_SKILL_NAMEfails nothing at all. - The skill tells an agent to run a command that posts to GitHub. Mitigated by
contract rather than convention: the amended skill requirement in
cli-feedbackkeeps showing the complete draft and receiving explicit confirmation a MUST. - Anonymization is instruction, not enforcement. An agent can still include a path it should have redacted. This is the same bound the existing spec accepts; making it mechanical would mean parsing the body the agent wrote, which is a different change.
Migration
None for data. openspec update installs the new skill and command on the next
run; openspec init installs them for new projects. Projects on a custom
profile keep their selection and are nudged, not modified.