Files
Clay Good 9b17c537c5 docs(openspec): make the feedback skill refine before it drafts
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>
2026-09-11 10:12:01 -05:00

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_DIR in src/core/profile-sync-drift.ts:28 is typed Record<WorkflowId, string>, so adding an id to ALL_WORKFLOWS without adding it there fails tsc. Four other maps carry the same information without that protection: the duplicate WORKFLOW_TO_SKILL_DIR in src/core/init.ts:111 (typed Record<string, string>), COMMAND_TO_SKILL_NAME in src/utils/command-references.ts:53, OPENSPEC_SKILL_NAMES in src/core/config.ts:3, and COMMAND_IDS in src/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 from ALL_WORKFLOWS.
  • feedback is the only workflow module with no command template. Every other src/core/templates/workflows/*.ts exports a getOpsx*CommandTemplate beside its skill template. getCommandTemplates() and COMMAND_IDS assume the pair, and profile-sync-drift reads a missing command file under delivery: '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) returns CORE_WORKFLOWS unconditionally for any non-custom profile and ignores customWorkflows entirely for core; DEFAULT_CONFIG.profile is 'core' (src/core/global-config.ts:45). An id added to ALL_WORKFLOWS alone reaches a user only if they open openspec config profile and tick it, which also flips them to custom.

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 feedback with 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.ts would 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 explore already works — its shared PLANNING_GUIDANCE says 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. explore already 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-creator calls 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-agents puts 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 the Record<WorkflowId, string> at profile-sync-drift.ts:28. The other guards are fixed-count assertions such as tool-detection.test.ts:33, which fail when you add an entry and stay silent when you skip one, and command-references.test.ts:92, whose list is hand-written and does not derive from ALL_WORKFLOWS — it omits propose, which is in the map. Omitting feedback from COMMAND_TO_SKILL_NAME fails 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-feedback keeps 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.